Class Booking

ClassTap

Fill Classes. Reclaim Your Time.

ClassTap is a brandable online booking and payment platform for independent instructors and small studios (ages 25–55) that streamlines scheduling, invoices and attendee management, automates waitlist-to-payment flows, prevents double-bookings, cuts administrative time by 60%, reduces no-shows 30%, and boosts monthly revenue by 15%.

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

ClassTap

Product Details

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

Vision & Mission

Vision
Empower independent instructors and small studios to effortlessly fill classes, reclaim time, and grow sustainable income and communities.
Long Term Goal
Within 4 years, empower 10,000 local instructors to cut administrative time by 60% and boost combined annual instructor revenue by $50 million.
Impact
For independent instructors and small studios, ClassTap cuts administrative time by 60%, reduces no-show rates by 30%, and boosts monthly revenue by 15% through automated booking, integrated payments, waitlist-to-payment conversion, and targeted reminders—turning freed time into sustained income and fuller classes.

Problem & Solution

Problem Statement
Independent instructors and small studios lose time and revenue to manual bookings, awkward payments, and frequent no-shows because marketplaces strip brand control and generic booking tools are complex, forcing costly spreadsheets and ad-hoc admin.
Solution Overview
ClassTap provides a brandable booking site with integrated payments and automated waitlist-to-payment workflows, eliminating awkward manual payments, preventing double-bookings, and turning freed spots into paying attendees without spreadsheets or extra admin.

Details & Audience

Description
ClassTap is a simple online booking and payment platform that streamlines scheduling, invoicing, and attendee management for local classes and workshops. It serves independent instructors and small studios seeking a brandable, easy way to accept bookings and payments. ClassTap eliminates double-bookings, cuts manual admin and reduces no-shows so organizers save time and boost revenue. Its native waitlist-to-payment automation converts freed spots into paid attendees automatically.
Target Audience
Independent instructors and small studios (25–55) who prefer brandable, low-complexity booking to reduce admin.
Inspiration
At a crowded weekend pottery workshop I watched an instructor scribble a paper waitlist as three eager students walked out because payment felt awkward. She paused, juggling clay and a phone, apologizing about invoices and spreadsheets. That small, deflating scene—lost sales, scattered lists, and stalled momentum—inspired a tiny, joyful tool that takes bookings, collects payments, and converts waitlisted spots automatically so teachers keep income and community intact.

User Personas

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

M

Multi-Location Maven Mia

- Age 35–44; owner-operator of 2–3 boutique studios - Urban/suburban mix; 8–20 instructors total - Annual revenue $500K–$1.5M across locations - Uses MacBook at HQ; iPhone on floor

Background

Started as a solo instructor, opened a second location after waitlists exploded. Outgrew spreadsheets and single-site tools while hiring managers. Learned the hard way when two studios double-booked a room.

Needs & Pain Points

Needs

1. Unified multi-location calendar with conflict prevention 2. Role-based permissions and approval workflows 3. Consolidated reporting and instructor payout automation

Pain Points

1. Cross-location double-bookings and room conflicts 2. Manual, error-prone multi-site payouts 3. Inconsistent branding across booking pages

Psychographics

- Obsessed with scalable, repeatable operations - Measures success by utilization and margins - Delegates, but demands brand consistency - Chooses tools that grow with her

Channels

1. LinkedIn Groups — studio ops 2. Instagram Reels — growth tips 3. Google Search — scheduling software 4. YouTube — management tutorials 5. Apple Podcasts — small business

R

Resource-Savvy Scheduler Sam

- Age 28–40; owner-coach of Pilates/cycle studio - 6–14 machines; 3–8 instructors rotating - 250–500 weekly bookings; evenings peak - iPad on floor; desktop for admin

Background

Lost thousands from double-booked machines and idle seats. Tried generic calendars that couldn't assign clients to equipment. Switched focus to utilization.

Needs & Pain Points

Needs

1. Per-equipment seat assignment and holds 2. Auto waitlist-to-payment per seat 3. SMS reminders and late-cancel controls

Pain Points

1. Machines double-booked or sit empty 2. Manual seat shuffling before class 3. Late cancels leave unsold seats

Psychographics

- Hates idle equipment and wasted time - Prioritizes fairness in seat assignments - Lives by tight, predictable schedules - Seeks automation over manual tweaks

Channels

1. Instagram Carousels — studio ops 2. TikTok — floor walkthroughs 3. Google Maps — local discovery 4. Facebook Groups — Pilates owners 5. YouTube — equipment tips

C

Cohort-Builder Casey

- Age 30–48; yoga/skill coach in college town - 10–24 students per cohort; 2 programs/quarter - Evenings and weekends; classroom-style space - Uses Zoom for makeups; Stripe for payments

Background

After drop-in churn, Casey shifted to structured cohorts to improve outcomes. Spreadsheets and scattered links caused missed payments and confusion.

Needs & Pain Points

Needs

1. Series packaging with fixed dates 2. Installment plans with auto reminders 3. Easy makeups and attendance rollovers

Pain Points

1. Chasing late installment payments 2. Linking sessions confuses registrants 3. Manual roster updates each week

Psychographics

- Outcome-focused, community-first mindset - Values commitment over casual attendance - Transparent pricing and expectations - Prefers structure with light flexibility

Channels

1. Instagram Stories — program launches 2. Eventbrite — discovery listings 3. Facebook Events — local reach 4. Mailchimp Campaigns — alumni list 5. Google Search — course keywords

F

Family-Programs Frankie

- Age 29–44; kids yoga/gymnastics owner - Suburban strip-mall studio; school-year seasonality - 60% bookings by mobile; evenings/weekends peak - Two part-time assistants; volunteer helpers

Background

Former school coach turned studio owner. Paper waivers and ad-hoc signups created lines and errors at check-in.

Needs & Pain Points

Needs

1. Guardian accounts with multiple dependents 2. Digital waivers tied to profiles 3. Automatic sibling/multi-class discounts

Pain Points

1. Parents book wrong child or age group 2. Lost or outdated paper waivers 3. Bottlenecks at after-school check-in

Psychographics

- Safety and compliance above everything - Over-communicates to keep parents confident - Community-centered, referral-driven growth - Prefers predictable, repeatable processes

Channels

1. Facebook Groups — local parents 2. Instagram Hashtags — neighborhood reach 3. Nextdoor — community events 4. Google Maps — reviews matter 5. Facebook Events — class signups

R

Retreat-Host Riley

- Age 32–52; yoga/wellness educator - 10–30 attendees per event; 1–3 quarterly - Mix of domestic and international venues - Relies on Instagram and email pre-sales

Background

An overbooked retreat with unpaid spots triggered chargebacks and chaos. Riley vowed to professionalize payments and confirmations.

Needs & Pain Points

Needs

1. Deposits with automated balance reminders 2. Tiered pricing and room types 3. Waitlist invites that auto-capture payment

Pain Points

1. Unpaid reservations after invite 2. Currency, timezone miscommunications 3. Chargeback exposure from unclear terms

Psychographics

- Experience curator, hospitality-minded - Risk-averse with cash flow - Values transparency and deadlines - Marketing-savvy storyteller

Channels

1. Instagram Reels — travel teasers 2. TikTok — behind-the-scenes 3. Google Search — retreat keywords 4. YouTube — vlogs and recaps 5. Mailchimp Campaigns — launch lists

G

Global-Gig Gabby

- Age 27–41; nomadic instructor/coach - Mix of Zoom and pop-ups; US/EU clients - MacBook and iPhone; airport Wi‑Fi reality - Income diversified: classes, privates, workshops

Background

Left corporate to tour coworking cities. Time-zone mix-ups and failed cross-border payments hurt trust and revenue.

Needs & Pain Points

Needs

1. Automatic time-zone conversion on listings 2. Multi-currency pricing and payouts 3. Calendar sync with travel itinerary

Pain Points

1. Clients miss sessions due to time confusion 2. Cross-border cards fail at checkout 3. Double-booked days across time zones

Psychographics

- Freedom-seeking, automation-first - Communicates asynchronously, promptly - Prioritizes clarity in times and prices - Comfortable experimenting with tools

Channels

1. Instagram Stories — travel updates 2. WhatsApp Broadcasts — class alerts 3. Nomad List Forum — peer advice 4. Reddit r/digitalnomad — tools talk 5. YouTube Tutorials — tool reviews

Product Features

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

SourceSync Merge

Connect Calendly, Mindbody, and calendar sources in minutes, then automatically deduplicate events and unify overlapping classes into a single master schedule. Choose one‑time import or ongoing sync to keep ClassTap as your single source of truth with zero double‑entry.

Requirements

Multi-Source Connector Onboarding
"As an instructor or studio admin, I want to connect my existing scheduling systems to ClassTap so that my classes flow in automatically without manual data entry."
Description

Provide guided setup to connect Calendly, Mindbody, and major calendar providers (Google, Outlook, Apple/iCal) within minutes. Support OAuth and API key flows, calendar/source selection, environment selection (e.g., Mindbody site), and mapping to ClassTap entities (instructors, locations, class types). Include connection testing, timezone detection, and granular scope selection (read-only vs read/write where applicable). Persist connections securely and expose a clear summary of linked sources to establish ClassTap as the central hub for subsequent merge and sync operations.

Acceptance Criteria
OAuth Connection for Google and Outlook Calendars
Given I am on the Multi-Source Connector setup and select Google or Outlook When I initiate OAuth Then I am redirected to the provider consent screen with the requested scopes clearly listed. Given I grant consent When the OAuth callback is received Then the connection is created, a refresh token is stored encrypted at rest, and the connected account email is displayed. Given consent is denied or the flow is canceled When redirected back to ClassTap Then the setup shows a non-blocking error state with a retry option and no connection is persisted. Given the connection is created When I click Test Connection Then a read of calendars succeeds within 10 seconds and returns at least one calendar or a zero-results message if none exist. Given the provider returns a transient error (4xx/5xx or rate limit) When testing the connection Then the UI shows a retriable error with provider code and the system retries up to 2 times with exponential backoff.
Mindbody Connection via API Key and Site Selection
Given I choose Mindbody When I enter a valid API key, site ID, and environment (sandbox or production) Then validation succeeds and credentials are stored encrypted at rest. Given the inputs are invalid When I submit Then field-level errors identify the invalid value(s) and no connection is saved. Given a valid connection When I click Test Connection Then requests to Mindbody Locations and Class Types endpoints return within 15 seconds and display the counts retrieved. Given multiple sites are available for the API key When I select a site Then only resources from the chosen site are used in subsequent mapping. Given the API key lacks required permissions When testing the connection Then the UI indicates missing scopes/permissions and links to setup documentation.
Source and Calendar Selection
Given I have authenticated a calendar provider When presented with available calendars Then I can multi-select specific calendars and must select at least one to proceed. Given I select calendars When I continue Then selections are persisted and displayed as tags with provider icon and calendar name. Given I attempt to select a calendar already connected under another account When I proceed Then the UI warns about duplication and prevents connecting the same calendar ID twice. Given no calendars are selected When I click Continue Then a blocking validation error is shown and I cannot proceed.
Entity Mapping to ClassTap (Instructors, Locations, Class Types)
Given source entities are fetched When the mapping step is displayed Then each external entity shows a suggested ClassTap match when an exact name/type match exists, with options to change or create new. Given I create a new ClassTap entity from the mapping When I save Then the new entity is created and immediately available for selection without leaving the flow. Given mappings are incomplete When I attempt to continue Then unmapped required entities are highlighted and I cannot proceed until all required mappings are resolved. Given mappings were previously saved When I revisit the setup Then prior mappings are preloaded and editable.
Timezone Detection and Normalization
Given I have connected at least one source When I run Test Connection Then the system detects the ClassTap org timezone and each source timezone and displays any mismatches. Given events contain explicit timezones When previewing sample events Then ClassTap normalizes all event start/end times to the org timezone while preserving original timezone metadata. Given a source lacks timezone metadata When testing Then I am prompted to select a timezone for that source before proceeding. Given events span a daylight saving transition When normalized Then displayed start/end times reflect correct offsets for the transition date.
Scope Selection: Read-Only vs Read/Write
Given a provider supports write access When I choose Read-Only Then any UI to publish changes back to the source is disabled and no write API calls are executed. Given I choose Read/Write When the connection test runs Then the system verifies write permission by confirming the access role for the selected calendar equals writer or owner without creating events. Given I change the scope selection When I save Then the new scope is persisted and reflected in the Linked Sources summary.
Linked Sources Summary and Secure Persistence
Given I complete onboarding When I view Linked Sources Then I see each connection with provider, account identifier, selected calendars or site, scope, last test timestamp, and status (Connected or Error). Given I click Disconnect on a source When I confirm Then tokens/keys are revoked or deleted, mappings remain but are marked inactive, and the source is removed from the active list. Given credentials are stored When persisted Then they are encrypted at rest and never displayed in plaintext in the UI or logs. Given a connection error occurs after onboarding When I open Linked Sources Then an error badge and details are shown with a Retry Test action that completes within 10 seconds or shows a retriable error.
Merge & Deduplication Engine
"As a studio admin, I want overlapping events from different systems to be merged into one master class so that my schedule is accurate and free of duplicates."
Description

Implement a deterministic engine that unifies overlapping or duplicate events from multiple sources into a single master class in ClassTap. Use configurable fuzzy matching and scoring across title, instructor, location, and time overlap, with tunable thresholds and tie‑breakers. Apply source priority rules and field-level merge strategies (e.g., title from Mindbody, attendee list union) to produce consistent results. Ensure idempotent processing, stable master IDs, and reversible merges, while preserving source references for round‑trip context and troubleshooting.

Acceptance Criteria
Merge Overlapping/Duplicate Events into One Master
Given ClassTap is connected to Calendly, Mindbody, and a calendar source And two events A (Calendly) and B (Mindbody) have title similarity ≥ 0.9, the same instructor, location similarity ≥ 0.8, and start times within 10 minutes And the merge threshold is set to 0.8 with ongoing sync enabled When the merge engine runs Then exactly one master class M is created for A and B And M appears once in the ClassTap schedule for the time window And M.sourceReferences contains both A and B And neither A nor B appear as separate classes in the master schedule
Configurable Fuzzy Matching, Thresholds, and Deterministic Tie-Breakers
Given weights are configured as title=0.4, instructor=0.3, location=0.2, timeOverlap=0.1 and the merge threshold is 0.75 And events C and D share the same title and instructor, are at the same location, and start within 5 minutes When the engine computes the match score for C and D Then score(C,D) ≥ 0.85 and C and D are merged And given events E and F only share the same title, have different instructors and locations, and start 45 minutes apart When the engine computes the match score for E and F Then score(E,F) ≤ 0.40 and E and F are not merged And given two candidate merge groups with equal final scores When tie-breakers are applied Then the source with higher priority is selected; if still tied, earliest start time wins; if still tied, the lexicographically smaller (sourceType, sourceEventId) wins
Source Priority & Field-Level Merge Rules
Given sourcePriority is Mindbody > Calendly > Calendar And field strategies are: title=Mindbody-preferred, description=highestPriorityNonEmpty, instructor=highestPriorityNonEmpty, location=highestPriorityNonEmpty, attendeeList=unionByEmail, tags=unionUnique When events from Mindbody and Calendly are merged into a master Then master.title equals the Mindbody title And master.description, instructor, and location are taken from the highest-priority non-empty values And master.attendees equals the deduplicated union of all source attendees by email with each attendee retaining a sourceTag indicating origin And all lower-priority conflicting values are preserved in the audit trail with field-level provenance
Idempotent Reprocessing Produces Identical Results
Given the same set of source events and the same merge configuration When the merge engine is executed N≥2 times consecutively Then the set of master classes, their masterIds, source-to-master mappings, and merged field values are identical across all runs And runs 2..N produce zero writes (no new masters, updates, or deletes)
Stable Master IDs Across Syncs and Non-Identity Updates
Given a master M is formed from source events A and B And identity keys are defined by the configured matching rules When a source updates non-identity fields (e.g., description, notes) such that the match score remains ≥ threshold Then M retains the same masterId and updates in place And when a source update reduces the match score below threshold (e.g., time shift beyond the allowed overlap) Then a new master is created as needed and the previous mapping is closed with a history record linking the old and new masters
Reversible Merge (Unmerge) with Audit Trail
Given a master M merged from sources A, B, and C When an admin triggers Unmerge on M Then M is dissolved and A, B, and C are restored as independent items eligible for re-merge per current rules And an audit record is created capturing prior masterId, sourceEventIds, timestamp, actor, and rationale And a Redo Merge action can reapply the previous decision to recreate M deterministically
Preservation of Source References & Round-Trip Context
Given any master class M When M is retrieved via API or UI Then M includes for each source reference: sourceType, sourceEventId, deepLink/sourceURL, lastSyncAt, createdAt, updatedAt, and field-level provenance And selecting a source reference opens the originating item in the external system when available And exports include the same references for troubleshooting
Sync Modes, Webhooks & Change Detection
"As a busy instructor, I want changes made in my external calendars to reflect quickly in ClassTap so that my master schedule always stays up to date."
Description

Offer one‑time import and ongoing sync modes with configurable backfill windows and schedules. In ongoing mode, prefer webhooks where available (Calendly, Google push notifications) with a polling fallback that respects rate limits. Detect and apply deltas (create, update, cancel, reschedule) reliably, including recurrence expansions and exceptions. Handle timezones and daylight saving transitions correctly, and ensure at‑least‑once processing with dedupe to prevent double‑entry. Provide resumable sync with pagination, checkpointing, and replay for robustness.

Acceptance Criteria
One‑Time Import with Configurable Backfill Window
Given a connected source and a backfill window of 30 days is configured When a one-time import is initiated Then only events with start time between now minus 30 days and now are imported And a summary is produced with counts for created, updated, deduplicated, and skipped events, and the total equals the number of events in scope And re-running the same one-time import is idempotent and does not create duplicate records
Ongoing Sync Scheduling and Execution
Given ongoing sync is enabled with a 5-minute schedule When the system is operating normally Then a sync run starts every 5 minutes with a tolerance of ±1 minute And overlapping runs are not allowed; subsequent runs wait or skip without data loss And each run persists a checkpoint (cursor/timestamp) after successful page processing
Webhook‑First with Polling Fallback
Given the source supports webhooks and ongoing sync is enabled When subscriptions are created Then webhook subscriptions are registered successfully and renewed before expiry When webhook events are delivered Then 95% of changes are applied within 5 minutes of receipt and none exceed 15 minutes under normal conditions Given webhooks are unavailable or fail When polling is used Then request rates never exceed the provider’s documented limits, 429/5xx responses trigger exponential backoff with jitter, and missed changes are recovered using since-cursor on the next successful poll
Delta Detection: Create, Update, Cancel, Reschedule
Given source events generate creates, updates, cancellations, and reschedules When the changes are processed Then the master schedule reflects the change types accurately with correct fields (title, instructor, capacity, location, start/end, join URL) And updates modify only the changed fields; reschedules preserve the event identity while changing time fields; cancellations mark events as canceled without deletion And processing is at-least-once with idempotency keys so each source change results in exactly one applied change in the master schedule And an audit log entry is recorded with source event ID, change type, processing status, and processed-at timestamp
Recurrence Expansion and Exceptions
Given a recurring series exists in the source with exceptions (modified or canceled instances) When syncing Then instances are expanded up to a 90-day horizon And single-instance modifications override the series template for that occurrence And single-instance cancellations remove only that occurrence, while series cancellations remove only future occurrences, leaving past ones intact And duplicate instances are not created when both series and instance endpoints emit events
Timezone and DST Correctness
Given sources in different timezones and DST transitions occur When events are imported and returned by the API Then stored times are normalized to UTC and source timezone metadata is retained And local start/end times returned by the API are correct before and after DST changes with no 1-hour drift, duplicate, or missing occurrences across the transition And rescheduling across timezones preserves the intended local time
Resumable Sync with Pagination, Checkpointing, and Replay
Given a sync run processes multiple pages and a transient error occurs mid-run When the run is retried Then processing resumes from the last successful checkpoint, previously applied pages are not reprocessed, and no changes are lost And pagination continues until the provider signals completion, persisting next-page tokens/cursors securely Given an operator initiates a replay from timestamp T When executed Then all changes since T are re-fetched and re-applied idempotently And metrics and alerts are emitted for run successes, retries, rate-limit backoffs, and failures
Master Schedule Data Model & Source Priority Rules
"As a platform admin, I want a clear data model with source-to-master mappings so that the system consistently decides which data wins and why."
Description

Define a normalized data model for master classes that references underlying source events, attendees, and metadata. Maintain a crosswalk of source IDs to master IDs to support upsert logic and traceability. Enable configurable source precedence and field-level ownership (e.g., capacity from Mindbody, description from Calendly) and automatically re-evaluate merges when source data changes. Expose clear state for each master record (source-derived vs overridden) to reinforce ClassTap as the single source of truth.

Acceptance Criteria
Initial Import Creates Master and Crosswalks
Given Calendly, Mindbody, and Calendar sources are connected and a one-time import is initiated When three source events represent the same class (same instructor email, normalized title match, and start time within ±5 minutes) Then exactly one master class record is created with a unique master_id And three crosswalk rows exist mapping (source, source_event_id) to that master_id And the master record references source attendees and metadata via foreign keys And rerunning the same import job does not create additional master records or crosswalk rows
Field-Level Ownership and Precedence
Given field ownership is configured as: capacity=Mindbody, description=Calendly, start/end time priority Mindbody > Calendly > Calendar When a lower-priority source updates a field it does not own Then the master field value remains unchanged and field_owner remains the configured owner When the owning source updates its field (e.g., capacity to 25) Then the master field updates to the new value within 60 seconds of detection and field_state is "source-derived" When the owning source value is null/empty Then the next available non-empty value by precedence is applied and field_owner is updated accordingly
Upsert Behavior and Crosswalk Updates
Given a master class exists with crosswalks to one or more source events When a source re-sends an event with the same source_event_id and updated data Then the master record is updated according to field ownership rules without creating a new master When a new source event matches the master by merge key (same instructor email, normalized title, start time within ±5 minutes) Then a new crosswalk row is added linking that source_event_id to the existing master without creating a duplicate master When a mapped source event is canceled/deleted at the source Then its crosswalk row is marked inactive and the master remains active if any mapped source event is still active; otherwise the master status becomes "canceled"
Automatic Merge Re-evaluation on Source Changes
Given two master records exist or a master has multiple mapped source events When a mapped source changes start/end time or title such that merge keys change Then the system re-evaluates within 120 seconds and either merges masters that now match or splits a master whose mappings no longer match And on merge, the earliest-created master_id is retained, all crosswalks and attendees are re-associated to it, and the merged-away master is archived with a redirect to the retained master And on split, a new master is created for the diverged mapping and only the relevant crosswalks/attendees are moved And an audit log entry records before/after mappings for each merge or split
Override Persistence and State Exposure
Given an admin overrides a master field (e.g., description) via ClassTap Then the field_state becomes "overridden" with recorded user_id and timestamp and the field_owner is null When any source updates that field afterward Then the master field value does not change and a suppressed update is logged When the override is removed Then the field recalculates based on current ownership/precedence and updates within 60 seconds And an API/DB representation is available per field exposing: state (source-derived|overridden), owner_source (if source-derived), and last_source_update_at
Ownership Rule Change Reconciliation
Given an admin updates source precedence or field-level ownership configuration When reconciliation is executed Then only fields with state "source-derived" are recalculated; overridden fields remain unchanged And affected master fields are updated to the new owner values where non-empty, falling back by precedence if empty And a preview endpoint/action returns counts of masters/fields to change before apply, and a summary after apply (updated, skipped, conflicted) And reconciliation of 10,000 master records completes within 10 minutes without timeouts
Idempotent Ongoing Sync and Performance
Given ongoing sync is enabled When a sync cycle runs with no source data changes Then zero master or crosswalk rows are inserted, updated, or deleted When 100 source events change across connected sources Then the incremental sync applies only necessary updates and completes within 60 seconds of detection And a unique constraint exists on (source, source_event_id) in the crosswalk and prevents duplicate mappings with safe retry logic (up to 3 retries) without creating duplicates
Conflict & Double-Booking Guardrails
"As a studio owner, I want the system to catch and help resolve booking conflicts across sources so that I don’t accidentally double-book instructors or rooms."
Description

Prevent instructor and room double-bookings by evaluating merged master classes against resource availability across all connected sources. Apply conflict detection during import and on updates, with temporary holds during webhook processing to avoid race conditions. Provide automatic suggestions (e.g., alternate time, room) and safe resolution flows (skip, reschedule, split, or override with justification). Integrate with waitlist-to-payment automation so that newly freed spots from external changes promptly trigger ClassTap’s conversions.

Acceptance Criteria
Prevent Instructor Double-Booking During Initial Import
Given multiple connected sources with overlapping instructor assignments for the same time windows When a one-time import to build the master schedule is executed Then no two master classes assigned to the same instructor overlap in time by ≥1 minute And conflicting source events are flagged with conflict type = "Instructor" And the master schedule contains at most one active class per instructor per time block And a conflict resolution task is created with options: Skip, Reschedule, Split, Override
Room Conflict Detection on Ongoing Sync Updates
Given ongoing sync is enabled and a room has an existing master class When an inbound update attempts to place another class in the same room with overlapping time Then the incoming change is quarantined in "Conflict" state and not published live And a notification is sent to organization admins and the affected instructor within 60 seconds And resolution options are presented with pre-validated alternatives (time or room) And the published master schedule remains free of room double-bookings
Atomic Webhook Processing with Temporary Resource Holds
Given parallel inbound updates affect the same instructor or room When ClassTap processes the updates Then a temporary hold/lock is acquired per resource and time slice with TTL = 45 seconds And event application is atomic and idempotent so retries do not duplicate classes And under 20 concurrent conflicting updates, no double-booking appears in the master schedule And holds are released within 5 seconds of completion or TTL expiry, with audit entries for acquire/release
Automatic Resolution Suggestions for Conflicting Events
Given a detected instructor or room scheduling conflict When a user opens the conflict details view Then at least 3 valid suggestions are generated when availability permits And suggestions honor buffers, class duration, instructor availability, and room capacity constraints And suggestions include: next available time today, next available room at the same time, earliest date matching the original time window And each suggestion displays an impact summary (affected attendees, instructor, room, time delta) And applying a suggestion saves successfully and updates the schedule within 3 seconds
Safe Resolution Flows: Skip, Reschedule, Split, Override with Justification
Given a class conflict is present When the user selects Skip Then the inbound change is ignored and the source link is marked as skipped with reason captured When the user selects Reschedule Then the new time is saved without creating overlaps and attendees/instructor notifications are queued within 2 minutes When the user selects Split Then the class is divided across time/room so no overlaps remain and attendee reassignment tooling is available When the user selects Override Then the system requires a justification of at least 10 characters and records user, timestamp, scope, and justification in the audit log And overridden overlaps display a warning badge on the master schedule until cleared
Conflict Audit Log and Notifications
Given any conflict detection or resolution action occurs When the action is completed Then an audit entry is written with resource, time window, source system, action type, user (if any), justification (if any), before/after states, and lock ID (if applicable) And the audit entry is visible in admin views within 10 seconds And notifications honor org preferences (email, in-app) and are deduplicated to max 1 notification per conflict per 10 minutes And users can trace from a notification to the specific conflict record
Waitlist-to-Payment Trigger on Freed Spots After Conflict Resolution
Given a class with a waitlist in ClassTap When capacity increases or a seat frees due to external changes or conflict resolution Then the waitlist-to-payment automation triggers within 60 seconds And eligible customers are invited in priority order with a payment hold window of 10 minutes And the flow is idempotent with no duplicate charges if multiple updates arrive And if capacity is consumed externally before payment, holds are released and customers are notified And conversion outcomes are recorded to analytics for attribution
Admin Merge Console & Manual Overrides
"As a studio admin, I want a console to review and adjust merge decisions so that I can correct edge cases and keep the schedule clean."
Description

Deliver an admin UI to preview imports, review proposed merges, and compare source fields side-by-side before committing. Allow manual overrides of merge decisions, rule tuning (thresholds, priorities), one-click unmerge/relink, and field locks to keep specific values authoritative in ClassTap. Include searchable logs of merge decisions, a dry-run mode for new connections, and role-based access controls to protect sensitive operations.

Acceptance Criteria
Import Preview & Commit
Given at least one external source is connected and has events in the selected date range When the admin opens the Merge Console Then the preview displays counts of new, updated, and potential-duplicate events per source and date range And proposed merge groups are listed with a 0–100 confidence score and side-by-side source field comparisons (title, start/end, location, instructor, capacity, price, description, tags, source IDs) And filters are available for source, date range, confidence band, and conflict status When the admin selects up to 200 items and clicks Commit Then the system applies merges/creates within 10 seconds and returns a summary of created/merged/skipped/failed counts And any failures list actionable error reasons and allow retry of failed items only And all actions are recorded in the audit log with actor, timestamp, and affected IDs
Manual Overrides & Field Locks
Given a proposed merge group is displayed When the admin chooses Override Then they can: force-merge selected records, exclude/split records from the group, set the preferred master record, and set field-level locks on ClassTap fields And overrides are persisted and immediately reflected in the preview results And locked fields are visually indicated and are not overwritten by subsequent syncs from any source And the audit log records the override type, fields affected, pre/post values, actor, timestamp And attempts by users without permission to apply overrides or locks are blocked with an error and logged
Rule Tuning with Live Recalculation
Given the admin has Merge Rules permission When they adjust similarity threshold (0–100), field weightings, or source priority order Then inline validation prevents invalid inputs and requires confirmation before saving And clicking Apply recalculates proposed merge groups within 3 seconds for up to 5,000 events in the selected range And the UI displays delta counts (before vs after) for create/merge/skip prior to persisting changes And saved rules are versioned with name, actor, timestamp, and change summary, and can be reverted to a prior version
Unmerge and Relink Operations
Given a merged ClassTap class is selected When the admin clicks Unmerge Then the system separates constituent source records without data loss in ClassTap-specific fields And attendees, payments, and internal references remain attached to the ClassTap master class and are correctly relinked post-unmerge And no duplicate attendee or payment records are created And the operation completes within 5 seconds for merged groups of up to 10 records And the action is logged with before/after linkage details and is undoable within 15 minutes unless conflicting changes occurred
Searchable Logs & Audit Trail
Given the admin opens Merge Logs When they filter by date range, object ID, source, action (merge/unmerge/override/rule-change/commit), user, or status (success/fail) Then the results return within 2 seconds for up to 10k records and show timestamp (UTC), actor, action, affected IDs, before/after field snapshots, and reason/error messages And exporting the current results to CSV completes within 10 seconds for up to 10k rows And sensitive values are masked according to role permissions, and masked fields are indicated And accessing or exporting logs without permission is blocked and recorded
Dry-Run Mode for New Connections
Given a new source connection is created When the admin selects Dry-Run and a date range Then the system processes a simulation without persisting changes and presents the full preview UI with Commit disabled And a predicted impact summary (create/merge/skip counts) is shown and can be downloaded as a report And the UI clearly indicates Dry-Run state across headers and action buttons And exiting Dry-Run re-enables commit, and all dry-run actions are logged
Role-Based Access Controls
Given organization roles are configured When a user attempts to access the Merge Console or perform actions Then permissions are enforced as follows: Owner/Admin = full access; Editor = view preview and logs only; Viewer = no access by default (configurable) And restricted actions (commit, overrides, rule tuning, unmerge/relink, field lock changes, log export) require elevated permissions and are blocked with clear errors for others And UI elements for unauthorized actions are hidden or disabled, and server-side authorization blocks direct API calls And role changes take effect within 1 minute and are reflected without requiring a full logout
Monitoring, Error Handling & Audit Trail
"As an operations manager, I want clear monitoring and audit trails so that I can quickly detect issues, understand what changed, and prove data integrity."
Description

Implement comprehensive observability for sync and merge flows, including health dashboards, latency and success metrics, and per-connection status. Provide structured error handling with retries, backoff, and dead-letter queues, plus admin notifications for failures and data inconsistencies. Maintain immutable audit logs of source payloads, merge outcomes, overrides, and user actions to support troubleshooting and compliance, with PII minimization and secure storage aligned to ClassTap’s privacy standards.

Acceptance Criteria
Health Dashboard: Per-Connection Status & Metrics
Given at least one source connection is configured When I open the SourceSync health dashboard Then I can see for each connection: status (Healthy/Degraded/Down), last successful sync timestamp (UTC), rolling 24h success rate (%), 95th percentile end-to-end sync latency (ms), and current backlog size (#) And the dashboard auto-refreshes every 60 seconds And status is computed as: Healthy if success rate >= 98% AND last successful sync age <= 5 minutes AND p95 latency <= 10000 ms; Degraded if any one threshold is breached; Down if no successful sync for > 15 minutes And I can filter by connection type (Calendly, Mindbody, Calendar) and search by connection_id And the underlying metrics are exposed at /metrics with Prometheus-compatible names and labels (connection_id, source_type, environment)
Error Handling: Retries, Backoff, and Dead-Letter Queue
Given a transient error occurs during ingestion or merge (e.g., HTTP 5xx, network timeout, rate limit) When the job fails Then it is retried up to 5 times with exponential backoff (initial delay 30s, multiplier 2.0, max delay 10m) And retries are idempotent via a stable idempotency key per source event And if all retries exhaust, the event is moved to the Dead-Letter Queue within 60 seconds and marked failed in metrics And permanent errors (e.g., schema validation failure) are not retried and are sent to the Dead-Letter Queue immediately And all retry attempts and outcomes are recorded in the audit log with timestamps and correlation_id
Notifications: Admin Alerts for Failures & Inconsistencies
Given any Dead-Letter Queue placement OR error rate > 2% over 5 minutes per connection OR status = Down When the alert condition is met Then an admin notification is sent to configured channels (email and Slack) within 2 minutes And the notification includes connection_id, source_type, severity, error class, last 3 error messages, affected record count, correlation_id, and deep links to the dashboard and audit log And alerts are de-duplicated to at most 1 alert per connection per 10 minutes, with a summary digest every hour And alert routing is configurable per environment (dev/stage/prod) and per connection
Audit Trail: Immutable, PII-Minimized, Secure
Given any source event is processed When ingestion, merge, override, or a user action occurs Then an append-only audit record is written with fields: event_id, correlation_id, connection_id, source_type, action_type, outcome (success/fail), timestamp (UTC), payload_hash (SHA-256), redaction_profile_id, merge_decision, deduplication_keys, user_id (when applicable) And raw payloads containing PII are not stored; only hashed or redacted snapshots per the redaction profile; PII at rest is encrypted with AES-256-GCM and keys managed by KMS And the audit log is tamper-evident via hash chaining (prev_hash, record_hash); any mutation attempt is rejected and logged And retention is configurable per environment with a default of 400 days; purge jobs verify chain integrity before deletion And read/write paths enforce TLS 1.2+; records are stored in a private network segment with IAM-scoped access
Traceability: Correlation, Search, and Replay
Given a processed record has correlation_id, source event_id, and connection_id When I query the audit API by any of these identifiers Then I can retrieve the full lineage: ingestion record -> merge decision -> resulting master schedule mutations -> notifications, with timestamps And links in the UI navigate between these entities within two clicks And replaying a Dead-Letter Queue item via the admin console reprocesses it exactly once using the original idempotency key, updates lineage, and writes a new audit record referencing the original (parent_audit_id) And replay is permitted only for users with role Admin or SupportTier2 and is fully logged
Access Control & Export: Audit Log
Given a user attempts to access audit data When the user lacks role AuditViewer or higher Then the request is denied with HTTP 403 and no record content is returned And when the user has sufficient role, they can list, filter (by date range, connection_id, correlation_id, action_type), and paginate results (cursor-based, default 100, max 1000) And exports can be generated in JSON or CSV for a specified date range up to 31 days and are redacted per policy, streamed within 60 seconds for up to 1,000,000 records, and signed with a checksum (SHA-256) in metadata And all access and export actions are themselves logged in the audit trail

AutoMap Fields

An AI‑assisted mapper that reads your existing class types, instructors, locations, capacities, and time zones, then proposes precise field matches in ClassTap. Review-and-approve in one screen to eliminate tedious manual mapping and prevent setup errors.

Requirements

Source Data Ingestion
"As a studio owner, I want to import my current classes, instructors, locations, capacities, and time zones from my existing tools so that I can set up ClassTap without re-entering data."
Description

Implement connectors to ingest existing class-related data from common sources (CSV upload, calendar ICS, direct API integrations, and manual paste). Normalize encodings and delimiters, handle headerless files, and support bulk import. Queue and chunk large files to ensure responsiveness, perform basic structural validation (required columns, date formats), and produce standardized interim objects for downstream AI mapping. Provide deduplication heuristics for near-identical rows and surface ingestion errors with actionable messages within ClassTap’s onboarding flow.

Acceptance Criteria
CSV Upload Normalization & Headerless Support
Given a CSV file encoded as UTF-8, ISO-8859-1, or Windows-1252, When uploaded, Then the system auto-detects and normalizes to UTF-8 without data loss. Given delimiters comma, semicolon, tab, or pipe with optional quoted fields, When uploaded, Then delimiter and quote style are auto-detected with at least 99% accuracy on the first 200 rows. Given a headerless file, When uploaded, Then the system detects missing headers and prompts for field mapping or infers headers from the first row preview. Given mixed line endings (LF/CRLF), When uploaded, Then rows are correctly parsed with no phantom blank rows. Given a valid file, When uploaded, Then a preview of the first 100 rows renders within 2 seconds. Given a malformed row, When processed, Then the row is flagged with row number and reason, and ingestion continues for remaining rows.
Calendar ICS & Direct API Ingestion Compliance
Given a valid .ics feed from Google, Outlook, or Apple Calendar, When ingested, Then VEVENTs are parsed including UID, SUMMARY, DTSTART/DTEND, RRULE, EXDATE, and VTIMEZONE. Given recurring events, When ingested, Then occurrences are expanded into individual instances within the selected date range with correct timezone offsets and DST. Given cancelled or updated events with matching UID/RECURRENCE-ID, When re-ingested, Then prior instances are updated or removed accordingly. Given time zone definitions in VTIMEZONE, When processed, Then startUtc and endUtc are computed accurately to the minute. Given a REST API source supporting pagination and rate limits, When ingested, Then the connector paginates until completion, respects rate limits via exponential backoff, and resumes from the last successful page on transient errors. Given API authentication (OAuth2 bearer or API key), When invalid or expired, Then a clear re-authentication prompt is shown and ingestion halts without partial data creation.
Manual Paste Import with Auto-Detection & Preview
Given tabular data pasted from Excel or Google Sheets, When pasted, Then delimiter and column boundaries are auto-detected and rendered in a preview grid. Given more than 100 columns or 50,000 rows pasted, When attempted, Then the user is prompted to upload a file instead with explanation of limits. Given pasted data with or without headers, When processed, Then the system offers header detection and manual mapping before import. Given the user confirms mapping, When imported, Then at least 1,000 rows are processed per second on modern desktops and progress is displayed. Given invalid rows, When detected, Then they are highlighted inline with row numbers and reasons and can be excluded before import.
Large File Queueing, Chunking & Progress Feedback
Given an upload larger than 50MB or more than 100,000 rows, When initiated, Then processing is queued and chunked into batches of up to 10,000 rows. Given queued processing, When running, Then the UI remains responsive (under 1 second input-to-response latency) and shows progress percentage updated at least every 2 seconds. Given a long-running job, When requested by the user, Then pause, resume, and cancel actions are available and honored within 5 seconds. Given transient processing failures on a chunk, When they occur, Then the chunk retries up to 3 times with exponential backoff and is skipped with error logging if retries fail. Given network interruption during upload, When resumed within 30 minutes, Then upload continues via resumable upload without resending completed parts.
Structural Validation & Actionable Error Messaging
Given a source (CSV, ICS, API, paste), When ingested, Then required fields for a class record include title/name, start datetime, end datetime or duration, and at least one of instructor or instructorRef; missing requirements cause validation errors. Given date/time values in multiple formats (ISO 8601, MM/DD/YYYY HH:mm, DD/MM/YYYY HH:mm), When parsed, Then values are normalized or flagged with specific parsing errors. Given validation failures, When surfaced in onboarding, Then each error message includes row/event identifier, field name, problematic value sample, and a suggested fix; messages are grouped and filterable. Given more than 1,000 errors, When presented, Then errors are summarized with sampling and a downloadable full error report (CSV/JSON) is available. Given warnings (non-blocking), When present, Then they are labeled as warnings and do not block import completion.
Deduplication Heuristics for Near-Identical Rows
Given multiple records that normalize to the same tuple (normalizedTitle, startUtc, instructorRef, locationRef), When ingested, Then they are considered duplicates. Given near-identical records (title similarity ≥ 0.90 and startUtc within ±5 minutes and same instructor), When ingested, Then the system flags as potential duplicates for user review. Given duplicates, When auto-merge is enabled, Then the first-seen record is kept and subsequent duplicates are merged, incrementing a duplicateCount and preserving source provenance. Given user review of duplicates, When keep/merge/skip actions are taken, Then decisions are applied consistently across the batch and summarized at completion. Given deduplication, When completed, Then no two standardized interim objects share the same composite key; a dedup summary (kept, merged, skipped) is displayed.
Standardized Interim Objects for Downstream AI Mapping
Given any ingested source, When processed, Then the system produces standardized interim objects with schema: tempId, sourceType, sourceId, title, description, startUtc, endUtc, timezone, capacity, instructorRaw, locationRaw, metadata, and parsingStatus. Given interim objects, When generated, Then fields are normalized (whitespace trimmed, encodings UTF-8, date-times in ISO 8601 UTC) and timezone is retained separately. Given source provenance, When captured, Then interim objects include a mapping to source column/field positions for traceability. Given successful batch creation, When stored, Then interim objects are accessible to the AutoMap component via an internal API with latency under 200 ms per 100 objects. Given invalid or partial records, When represented, Then parsingStatus reflects success/warning/error with codes, and objects with errors are excluded from downstream mapping by default.
Schema & Entity Detection
"As a new ClassTap user, I want the system to recognize what each column or field represents even if it’s named differently so that my setup maps correctly without manual effort."
Description

Use AI/NLP to identify and extract entities (Class Type, Instructor, Location, Capacity, Time Zone) and their attributes from heterogeneous source data. Recognize synonyms and aliases (e.g., “Teacher”→Instructor, “Room A”→Location), parse semi-structured strings (titles that bundle date/time), and standardize outputs to ClassTap’s canonical schema. Provide fallback rule-based parsing when confidence is low. Emit a normalized candidate schema with entity relationships for matching.

Acceptance Criteria
Entity Extraction from Heterogeneous Sources
Given a mixed source dataset containing CSV, JSON, and XLSX files with fields representing class types, instructors, locations, capacities, and time zones on a labeled validation set of ≥ 500 records When the system ingests and processes the dataset Then it identifies and extracts all five entity types and their attributes into ClassTap’s canonical schema with ≥ 95% precision and ≥ 90% recall And it standardizes capacity to integer, time zone to IANA name, and trims/normalizes strings for consistent matching And it produces a machine-readable processing summary including record counts per entity, precision/recall, and confidence score histogram
Synonym and Alias Resolution
Given a configurable alias map including examples such as Teacher/Trainer/Coach → Instructor, Room A/Studio 1 → Location, TZ/Timezone → Time Zone, and Class/Offering → Class Type And a dataset containing both known and previously unseen synonyms/aliases When the system performs entity type classification and field name mapping Then known aliases are mapped with 100% accuracy And unseen synonyms are mapped with ≥ 90% accuracy at confidence ≥ 0.80 And any mapping with confidence < 0.80 is flagged as Needs Review and not auto-applied And all mappings include the original token, resolved canonical name, and confidence score in the audit log
Semi-Structured Title Parsing
Given records whose titles bundle multiple attributes (e.g., "Yoga Flow with Sam — Mon 7/08 6:30 PM PST — Room A — Cap 18") When the system parses titles Then it extracts class type, instructor, start datetime, time zone, location, and capacity, assigning them to the canonical fields And it converts the parsed local date/time and time zone to ISO 8601 UTC and stores the IANA time zone separately And it achieves ≥ 92% correct full-field parses across a labeled set of ≥ 200 such titles And any record with partial or failed parse is flagged with which fields are missing and the parse confidence per field
Confidence Scoring and Rule-Based Fallback
Given entity extraction with per-field confidence scores And a fallback rule-based parser configured for capacity, time zone, and location patterns When any field’s confidence < 0.80 Then the system invokes the rule-based parser for that field And if the combined confidence remains < 0.80, the field is marked Needs Review and excluded from auto-mapping actions And the final output includes which parser(s) were used per field and before/after confidence values And ≥ 95% of low-confidence cases receive a fallback attempt within the same processing run
Normalized Candidate Schema and Relationships
Given a processed dataset When the system emits the candidate schema Then the output conforms to the current canonical JSON Schema for ClassTap with required fields present for Class Type, Instructor, Location, Capacity, and Time Zone And entity relationships are present and referentially intact (100% of foreign keys resolve): classes reference instructors and locations, capacities tie to class instances or locations as configured And the payload passes JSON Schema validation with 0 errors And duplicate entities (e.g., same instructor alias) are deduplicated with a deterministic stable identifier and a merge log
Performance and Scalability
Given a single worker with 4 vCPU and 8 GB RAM And an input of 50,000 records across mixed formats totaling up to 1 GB When the system runs entity detection and normalization Then total processing time is ≤ 10 minutes end-to-end And P95 per-record processing latency is ≤ 50 ms And peak memory usage remains ≤ 6 GB And the system streams output so the first 1,000 normalized records are available within ≤ 30 seconds
Time Zone Standardization and Edge Cases
Given inputs with time zone expressions including IANA names (e.g., America/Los_Angeles), abbreviations (e.g., PST/PDT), and numeric offsets (e.g., UTC-8, -0800) And events near DST transitions When the system parses and standardizes time zones Then it resolves to a valid IANA time zone and computes the correct UTC timestamp for 100% of test cases with explicit IANA input And achieves ≥ 99% accuracy for abbreviation/offset inputs using location and date context And ambiguous or conflicting time zone information is flagged Needs Review and not auto-applied
Match Suggestions with Confidence
"As an instructor, I want the system to propose field matches with clear confidence indicators so that I know what I can accept instantly and what needs my review."
Description

Generate suggested mappings from source fields to ClassTap fields with per-suggestion confidence scores and rationale. Display top alternatives for each field, allow threshold configuration (auto-accept above X%), and flag ambiguities for review. Consolidate duplicate entities (e.g., “Yoga Basics” vs “Basics—Yoga”) and propose merges. Persist interim suggestions so users can resume later, and expose an API for programmatic consumption of mapping results.

Acceptance Criteria
Confidence Scores and Rationale Shown for Each Suggestion
Given source fields have been scanned and suggestions are generated When the mapping screen is opened Then each suggestion displays a numeric confidence score from 0 to 100 with one decimal precision And the score is visible adjacent to the proposed ClassTap field And a rationale panel is available per suggestion that lists at least two matching signals (e.g., token similarity, synonym match) and highlights matched tokens And any suggestion lacking a confidence score or rationale is omitted from the list and logged as an error
Top Alternatives Listed per Source Field
Given suggestions exist for a source field When the user expands alternatives for that source field Then the UI shows the top 3 alternatives sorted by descending confidence, each with its score And selecting an alternative immediately updates the pending mapping for that source field and marks it as a manual override And the alternatives panel provides access to the rationale for each alternative on hover or click
Auto-Accept Threshold Configuration
Given a configurable auto-accept threshold with a default of 85% When the user sets the threshold to any value between 50% and 100% Then any suggestion with confidence >= the threshold is auto-accepted and applied And suggestions with confidence < the threshold remain in Needs Review And the UI displays counts for Auto-accepted, Needs Review, Ambiguous, and No Match And changing the threshold re-evaluates the suggestions and updates counts within 2 seconds without losing prior manual overrides
Ambiguity Detection and Review Flagging
Given generated suggestions with ordered alternatives When the confidence gap between the top two alternatives is < 5 percentage points or the top confidence is < 60% Then the item is flagged as Ambiguous and added to the Review queue And ambiguous items are excluded from auto-accept regardless of the threshold until a user selects a target explicitly And the ambiguity flag displays a tooltip stating the reason (low gap or low confidence) And once the user selects a target, the ambiguity flag is cleared and the status updates accordingly
Duplicate Entity Consolidation and Merge Proposal
Given the imported dataset contains near-duplicate entities (e.g., name reordering, punctuation variants) When the similarity score between two entities is >= 90% according to the system's matching algorithm Then a merge group is proposed listing the candidates, evidence of similarity, and a recommended primary entity And the user can accept or reject each proposed merge; accepted merges mark non-primary entities as aliases of the primary And after accepting, only a single target mapping is created for the group and all references resolve to the primary And an undo option is available for any accepted merge until the mapping session is finalized
Draft Persistence and Resume
Given a user has in-progress mapping work When the user changes the threshold, resolves an ambiguity, accepts a merge, or sets a manual override Then the session state (suggestions, selections, thresholds, flags, merges) is autosaved within 2 seconds And if the user closes the browser or navigates away and returns within 30 days Then reopening the mapping session restores the exact prior state and progress metrics And autosave operations are idempotent and do not create duplicate suggestions or merges
Programmatic API for Mapping Results
Given an authenticated client requests mapping results for a valid session ID When the client calls the mappings API endpoint with that session ID Then the API responds 200 with a payload that includes for each source field: selected_target_id (nullable), selected_label, confidence (0–100), rationale (string), status (auto_accepted|manual_override|needs_review|ambiguous|no_match), alternatives [{target_id, label, confidence}], and merge_group_id (nullable) And the API supports pagination with limit and cursor parameters and returns total_count And unauthorized requests return 401; invalid or expired session IDs return 404 And the API responds within 800 ms for sessions up to 5,000 source fields
One-Screen Review & Approval
"As an admin, I want to review and approve all mappings in one place with previews so that I can finish setup quickly and confidently."
Description

Provide a unified UI to review, edit, and approve all suggested mappings in a single screen. Support bulk-approve by confidence, inline editing, search/filter, keyboard shortcuts, and accessibility (WCAG 2.1 AA). Show live previews of how mapped data will appear in ClassTap (e.g., class cards, schedules). Include progress indicators, sticky action bar, and guardrails preventing navigation loss. On approval, commit mappings and trigger downstream setup (schedule creation, instructor assignment).

Acceptance Criteria
Unified One-Screen Review with Sticky Action Bar, Progress, and Guardrails
Given suggested mappings exist, When the Review & Approve screen loads, Then all entity types (class types, instructors, locations, capacities, time zones) are displayed in a single scrollable view without navigating to other pages. And the primary action bar remains visible (sticky) while scrolling and is operable via keyboard and screen readers. And a progress indicator shows Total, Reviewed, Approved, Edited, Needs Attention and updates within 200 ms after any action. Given there are unsaved changes, When the user attempts to navigate away (browser back, refresh, external link, close tab), Then a confirmation dialog blocks navigation and offers Stay/Leave; choosing Stay preserves state; choosing Leave discards changes intentionally. Given the page reloads after unsaved changes, When it reopens within the same session, Then in‑progress edits are restored. The screen meets WCAG 2.1 AA: logical focus order, labels for controls, contrast ≥ 4.5:1, visible focus indicators, no keyboard traps, full keyboard operability, reflows at 320px width, and text resizable to 200% without loss of content or functionality.
Bulk Approve by Confidence Threshold
Given confidence scores (0–100) per suggestion, When the user sets a confidence threshold T and applies bulk select, Then all items with score ≥ T are selected and items with score < T remain unselected. The default threshold is 90% on first load. The selection summary shows total to approve by entity type and excludes items with validation errors or conflicts. When the user clicks Approve Selected, Then only selected items transition to Approved and progress updates within 200 ms. When the threshold changes, Then the selection updates immediately and any manual selections outside the threshold are preserved unless the user chooses Recalculate selection.
Inline Editing with Validation
Given a suggested mapping, When the user invokes inline edit, Then the field renders an editor with typeahead for existing entities and allows new entity creation where permitted. On save (Enter, Ctrl/Cmd+S, or Save control), Then validation enforces: required fields not empty, names unique within entity type, capacity is a positive integer, and time zone is a valid IANA identifier; invalid input shows inline error text and blocks save. Upon successful save, Then the item status becomes Edited, the change persists, and the confidence label reflects Edited. When the user cancels (Esc or Cancel control), Then the value reverts to the last saved state.
Search and Filter Across Entities
Given the search input is focused, When the user types at least 2 characters, Then items are filtered across all entity types with matches highlighted and results render within 300 ms for up to 5,000 suggestions. Filters include Entity Type, Status (Unreviewed, Approved, Edited, Needs Attention), Confidence Range, and Has Errors; multiple filters can be combined. When Clear All is activated, Then the full, unfiltered list is restored and the visible results count updates accordingly.
Keyboard Shortcuts Coverage
The following shortcuts are available and discoverable via a ? help overlay: J/K to move next/previous item; Space to toggle selection; / to focus search; Enter to save inline edit; Esc to cancel inline edit or close dialogs; Ctrl/Cmd+Enter to approve selected; Shift+? to open shortcuts help. All shortcut-triggered actions are also available via clickable controls; shortcuts are disabled inside text inputs; focus states are visible; ARIA live regions announce state changes where appropriate.
Live Preview Accuracy
Given a mapped item is edited or approved, When the change is made, Then the Live Preview updates within 300 ms to reflect class cards and schedules exactly as they will appear (title, instructor, location, capacity, local time with time zone and DST). Preview elements are read-only, accessible with correct roles and labels, and meet AA color contrast; images have alt text or are marked decorative. When a list item is hovered or focused, Then the preview highlights the corresponding card entry.
Approval Commit and Downstream Triggers
Given at least one mapping is in Approved state, When the user clicks Finalize and confirms, Then the system commits all Approved mappings atomically and prevents duplicate submission (disabled button and idempotent backend). On success, Then downstream setup is triggered: schedules are created, instructors assigned, and time zones applied; operations for up to 1,000 mappings complete within 2 minutes. A completion summary displays counts of committed, skipped, and failed items with error reasons, links to retry failures, and a view of audit logs; if processing is asynchronous, a progress indicator is shown until complete. On partial failure, Then successful commits remain, failed items are clearly marked, and the user can safely retry without duplicating previously committed records.
Validation & Conflict Prevention
"As a studio manager, I want the system to catch conflicts and errors in my mappings so that I avoid double-bookings and incorrect schedules."
Description

Validate proposed mappings for logical consistency before commit. Detect overlapping schedules by instructor/location, invalid capacities, timezone inconsistencies, and duplicate instructors or class types. Provide contextual error messages and auto-fix suggestions (e.g., normalize time zones, merge duplicates). Block commit on critical errors, allow waivers for non-critical warnings, and write outcomes to an audit log.

Acceptance Criteria
Overlap Detection by Instructor and Location
Given proposed mappings contain two classes with the same instructor where startA < endB and startB < endA in the same timezone When validation runs on the review-and-approve screen Then the system flags a Critical error for each instructor overlap with code MAP-OVERLAP-INSTRUCTOR including instructorId, classIds, locationIds, start/end times, and a fix suggestion And intervals that meet endA == startB or endB == startA are not flagged as overlaps And each overlapping pair is reported once only (no duplicate error rows)
Capacity Validation and Normalization
Given a proposed class capacity is non-integer, zero, or negative When validation runs Then a Critical error MAP-CAPACITY-INVALID is shown with a suggestion to set capacity to a positive integer (default 1) Given a location has a known maxCapacity and a class capacity exceeds it When validation runs Then a Critical error MAP-CAPACITY-EXCEEDS-LOCATION is shown with a one-click auto-fix to clamp to location maxCapacity Given user applies the auto-fix When revalidation runs Then the capacity errors for that class are cleared and the new capacity is displayed in the preview
Timezone Consistency and Normalization
Given a class mapping has a timezone different from its location timezone When validation runs Then a Warning MAP-TZ-MISMATCH is shown with an auto-fix to normalize the class to the location timezone and display converted start/end times Given a class has no timezone and the location timezone is known When validation runs Then a Warning MAP-TZ-MISSING is shown with an auto-fix to assign the location timezone Given both class and location timezones are missing/invalid When validation runs Then a Critical error MAP-TZ-UNRESOLVED is shown and commit is not permitted until resolved And after applying timezone normalization, any new overlaps created are detected by the overlap rule
Duplicate Instructors Detection and Merge
Given two or more instructor records in proposed mappings match by (email case-insensitive) OR (normalized full name AND phone) OR (externalId) When validation runs Then duplicates are listed with a merge preview and severity: Critical if email matches; Warning otherwise, codes MAP-DUP-INSTRUCTOR-EMAIL / MAP-DUP-INSTRUCTOR-NAME And selecting Auto-merge consolidates to a single instructor, preserving canonical identifiers and re-pointing mapped classes in the preview And marking "Not a duplicate" downgrades/hides the warning for that pair with a recorded justification
Duplicate Class Types Detection and Merge
Given two or more class types match by normalizedName (lowercase, trimmed, punctuation removed) and equal duration When validation runs Then duplicates are listed with a merge preview and severity: Critical if externalId collides; Warning otherwise, codes MAP-DUP-CLASSTYPE-ID / MAP-DUP-CLASSTYPE-NAME And selecting Auto-merge consolidates the class type and updates affected class mappings in the preview And marking "Not a duplicate" records a waiver for that pair
Commit Blocking, Waivers, and Severity Handling
Given any Critical validation errors exist When the user views the review-and-approve screen Then the Commit action is disabled and a banner summarizes Critical and Warning counts Given only Warnings remain When the user attempts to Commit Then the system requires explicit waiver per warning category (checkbox) and a reason text (min 5 characters) before enabling Commit And after all required waivers are provided, Commit becomes enabled and proceeds without Critical errors
Audit Logging of Validation and Commit Outcomes
Given validation runs or a commit is attempted When the event completes Then an audit log record is written containing: timestamp, userId, sessionId, input dataset hash, counts by severity, list of errors/warnings (type code and affected IDs), auto-fixes applied (before/after), waiver decisions, and final outcome (Committed/Blocked) And audit log records are append-only (new runs create new entries without modifying prior entries) And an auditor can retrieve the latest record for the session and see fields populated as specified
Reusable Mapping Profiles
"As a returning user, I want to save my mapping configuration so that future imports are nearly automatic."
Description

Allow users to save approved mappings as named profiles tied to a data source. On subsequent imports from the same source, auto-apply the profile and only surface deltas for review. Support versioning, cloning, and organization-level sharing of profiles. Provide export/import of profiles as JSON for migration between accounts.

Acceptance Criteria
Auto-Apply Profile on Repeat Import
Given a saved mapping profile named "Studio A Default" tied to data source "MindBody:acct-123" and set as Active When the user initiates a new import from data source "MindBody:acct-123" and reaches the AutoMap step Then the system auto-applies the mappings from "Studio A Default" (latest Active version) without user input And the UI displays "Profile: Studio A Default (v3) auto-applied" with a link to details And auto-apply completes within 2 seconds for datasets up to 200 mappings And 100% of previously mapped fields are populated exactly as in the profile
Delta Surfacing and Approval
Given profile "Studio A Default" (v3) was previously applied to source "MindBody:acct-123" And the current import contains schema changes (added, removed, renamed, or type-changed fields) When the system evaluates the incoming fields against v3 Then only the changed items (deltas) are surfaced for review, with unchanged mappings collapsed by default And each delta is labeled as "Added", "Removed", "Renamed", or "Type Change" with old→new values And the user can approve all deltas in one action or approve/reject individually And upon approval, the applied mapping reflects decisions immediately and import can proceed without reloading
Profile Versioning and Rollback
Given a profile has versions v1..vN with audit metadata (version, author, timestamp, change summary) When a user modifies mappings and saves Then a new immutable version vN+1 is created and set as Active only if explicitly chosen And prior versions remain selectable for future imports And a user with permission can set vK (K≤N+1) as Active default for the tied data source And switching Active version logs an audit event and impacts subsequent auto-apply behavior And rollback to a prior version does not alter historical audit records
Clone Profile for Variant Use
Given an existing profile "Studio A Default" (v3) When the user selects "Clone" and provides a new name "Studio A Weekend" Then a new profile is created with identical mappings from v3 and versioned as v1 And the data source binding is optional: the user can keep the original binding or assign a new source before saving And if the provided name conflicts, the system suggests an available unique name by appending a numeric suffix And the clone action is recorded with source profile, actor, and timestamp
Organization-Level Sharing and Permissions
Given an organization with roles (Admin, Manager, Member) When an Admin marks a profile as Shared Then all users with Import permission can discover and apply the profile And only Admins and the profile Owner can edit, version, or delete the profile And Members can view history and apply but cannot modify mappings or sharing settings And access control is enforced in both UI and API, returning 403 on unauthorized actions And share/unshare events are logged with actor, timestamp, and scope
Export/Import Profiles as JSON for Migration
Given a profile with versions v1..vN exists When the user exports the profile Then the system generates a JSON file conforming to schema version S with checksum and includes all versions, metadata, and source bindings (excluding secrets) And when importing that JSON into another account Then the file is validated against schema S and checksum before processing And on name or source-identity conflict, the user is prompted to choose: Rename, Overwrite (if permitted), or Keep Both And invalid or incompatible files are rejected with a descriptive error code and message And successful import recreates the profile with full version history and sets no Active source by default unless explicitly confirmed
Data Source Identity Matching and Disambiguation
Given a profile is tied to a data source identity defined as {connector_type, source_id} When a new import starts Then auto-apply triggers only on an exact identity match And if multiple profiles match (e.g., multiple Active profiles for the same source), the user is prompted to choose a profile before apply And if no exact match exists, no profile is auto-applied and the system suggests the top 3 closest candidates by similarity with explicit user confirmation required And if detected source-level context (timezone/location) differs from the profile, those differences are flagged as deltas for review
Audit Trail & Rollback
"As an account admin, I want a history of mapping changes with rollback options so that I can recover from mistakes and maintain accountability."
Description

Record all mapping actions, including who approved what and when, original vs final values, and system-generated suggestions. Provide a per-import timeline and the ability to rollback to a prior mapping state. Expose exportable logs for support and compliance, and integrate with ClassTap’s notifications to alert stakeholders of significant changes.

Acceptance Criteria
Audit event logging for mapping approvals and edits
Given a signed-in user with mapping permissions is approving, overriding, rejecting, or creating a mapping within AutoMap on an active import When the user confirms the change Then the system writes one immutable audit event with fields: event_id, import_id, entity_type, entity_identifier, field_name, original_value, suggested_value, final_value, action_type, user_id, user_display_name, user_role, timestamp_utc (ISO 8601), source (ui|api), correlation_id, ai_suggested (true|false), final_differs_from_suggestion (true|false) And the audit event is visible in the import timeline within 2 seconds of confirmation And audit events cannot be edited or deleted via UI or API; only appended And if the action fails, no audit event is created and the user is shown an error message
Per-import audit timeline and diff view
Given an import session with recorded audit events When a user opens the audit timeline Then events are sorted by timestamp_utc descending and secondarily by event_id descending And the user can filter by action_type, user_id, entity_type, field_name, and date range And selecting an event shows a before/after diff highlighting changed values And pagination loads 50 events per page with total count displayed And an empty state is shown when no events match filters And the first page of up to 500 events loads within 2 seconds at P95
Role-based rollback to prior mapping snapshot
Given there exists at least one prior snapshot of mapping state for the import and the user has Owner or Admin role When the user selects a snapshot timestamp and confirms rollback by typing the confirmation phrase Then the system locks mapping for the import, prevents concurrent edits, and begins rollback And mapping records are reverted exactly to the selected snapshot And a new audit event of type rollback is appended with from_snapshot_id, to_snapshot_id, performed_by, reason, timestamp_utc And the lock is released after completion and the timeline reflects the rollback And the operation is atomic; on failure, no partial changes are committed and the user sees an error with retry option And rollback completes within 30 seconds for up to 10,000 mapped rows
Export audit logs for compliance and support
Given a user with permission (Owner, Admin, or Support) selects an import and filter set When the user requests an export in CSV or JSON format Then the generated file contains only events matching the filters and includes columns/keys: event_id, timestamp_utc, import_id, action_type, user_id, user_display_name, entity_type, entity_identifier, field_name, original_value, suggested_value, final_value, source, correlation_id And CSV output follows RFC 4180 escaping; JSON is UTF-8 encoded and valid And exports up to 100,000 events complete within 60 seconds and are downloadable via a secure URL And secrets or access tokens are not included in the output And the export action itself is recorded as an audit event
Notifications for significant mapping changes
Given significant change is defined as rollback, bulk approval of 20+ mappings, or override that alters capacity or time zone When such an event occurs Then in-app notifications are delivered to workspace Owners and Admins within 10 seconds and email within 2 minutes, respecting user notification preferences And the notification includes actor, action, import_id, count of affected mappings, timestamp_utc, and a link to the audit timeline And duplicate notifications for the same action are suppressed within a 5-minute window And notification delivery failures are retried up to 3 times and logged as audit events
Search and advanced filtering of audit logs
Given an import with 10,000+ audit events When a user searches by keyword or filters by user, action_type, entity_type, field_name, or date range Then results return within 2 seconds at P95 for the first page And search matches on user_display_name, entity_identifier, original_value, suggested_value, and final_value (case-insensitive, prefix-supported) And combining multiple filters narrows results (logical AND) And clearing filters resets the timeline to default sort

BrandSnap Palette

Drop in a website or logo and instantly generate an accessible, on‑brand booking theme—colors, fonts, buttons, and image treatments. See live previews as you tweak, so your booking flow looks polished from day one without a designer.

Requirements

Brand Asset Ingestion from URL/Logo
"As an instructor setting up my booking page, I want to drop in my website or logo so that the system instantly picks up my brand look without me configuring colors and fonts by hand."
Description

Allow users to paste a website URL or upload a logo to automatically extract brand signals (primary/secondary colors, accent hues, typography hints, favicon/logo imagery). The system crawls the homepage (robots-aware), parses CSS for declared colors and font families, analyzes uploaded logo imagery with color clustering, and consolidates candidates into a ranked brand palette and font suggestions. Handles timeouts, redirects, and CORS via a server-side fetcher with caching and rate limiting. Outputs normalized assets to the BrandSnap pipeline and stores only derived metadata (no full-site mirroring). This provides an instant on-brand starting point and reduces manual setup time within ClassTap’s theming wizard.

Acceptance Criteria
URL Crawl: Extract Colors and Fonts
Given a publicly accessible homepage URL using HTTP(S) When the user submits the URL for brand analysis Then the server-side fetcher retrieves the root page HTML and up to 3 same-origin CSS files referenced by the page And Then declared color values (hex, rgb[a], hsl[a], CSS variables) are extracted and normalized to hex; at least the top 6 unique colors by usage frequency are identified And Then declared font-family stacks are extracted and reduced to primary families; at least one site-declared family is returned if present And Then the end-to-end analysis completes within 10 seconds for pages <= 1 MB HTML and <= 500 KB CSS total under a 100 ms latency test profile
Logo Upload: Color Clustering
Given a user uploads a logo image (SVG, PNG, or JPG) up to 5 MB When analysis is initiated Then SVGs are rasterized to a max dimension of 512 px for sampling; transparent pixels and uniform background regions are excluded And Then color clustering returns 3–6 dominant colors sorted by dominance and normalized to hex And Then near-neutral colors (|L*−50| <= 5 or chroma < 5) are de-emphasized unless dominant in the logo And Then the output includes a WCAG contrast check against white and black, flagging at least one text-safe color (contrast >= 4.5:1) if available
Robots and Redirect Handling
Given robots.txt at the target domain disallows crawling of '/' When the user submits the URL Then the system does not crawl or fetch linked assets and returns a respectful message prompting alternative inputs (e.g., logo upload) Given robots.txt allows crawling of '/' When the URL responds with redirects Then the system follows up to 3 redirects and uses the final URL; if a canonical link is present, it is used for caching keys Then only the root page is fetched; no additional paths are crawled
Resilience: Timeouts, Retries, and CORS via Server Fetcher
Given transient network errors (DNS timeout, TCP reset, 5xx) When fetching website assets server-side Then each request uses a 10-second timeout and performs one retry on transient errors And Then all network requests originate from the server component; the client performs no cross-origin requests to the target site Given a final 4xx/5xx after retry or a timeout occurs Then the UI displays a non-blocking error, offers logo upload and manual color entry, and the analysis job is marked Failed with a machine-readable error code
Consolidation and Ranking of Brand Candidates
Given colors from website parsing and logo clustering are available When consolidating candidates Then colors are deduplicated using DeltaE2000 <= 2.0 and conflicts resolved by priority (logo > CSS variables > remaining CSS usage) And Then the final palette includes labeled slots: primary (1), secondary (1–2), accent (2–4), neutrals (1–2), totaling 6–8 colors And Then font suggestions prioritize site-declared families; if unavailable for web, a nearest Google Fonts alternative is provided with mapping rationale and confidence >= 0.6 And Then each color and font suggestion includes a confidence score (0.0–1.0) and a provenance tag (logo, css-var, css-rule)
Output Normalization and Storage Constraints
Given a successful analysis When persisting results Then only derived metadata (hex colors, labels, confidences, font family names, font source/provider, favicon/logo dominant colors, canonical URL) are stored And Then raw HTML, CSS, and image binaries are not persisted beyond transient processing and are hard-deleted within 24 hours And Then the normalized payload conforms to the BrandSnap pipeline schema v1 and is handed off via internal event within 2 seconds of persistence And Then database records contain no full-site mirrors, page text, or non-essential PII and are tagged with a 90-day retention policy
Caching and Rate Limiting Behavior
Given a completed analysis for a canonical domain exists in cache within 24 hours When the same domain is analyzed again Then the result is returned from cache within 500 ms and includes cache metadata (age, source) And Then per-user limits of 5 analyses/minute and per-domain limits of 10 analyses/hour are enforced; exceeding returns HTTP 429 with a Retry-After header And Then users can request a re-scan which invalidates the cache entry and enqueues a fresh job; cache keys are normalized to scheme-less, lowercased registrable domains
Accessible Palette Generator & Tokenization
"As a studio owner, I want the system to generate an accessible color scheme automatically so that my booking flow looks professional and meets accessibility standards without guesswork."
Description

Generate an accessible, on-brand theme from extracted inputs by creating semantic color tokens (primary, secondary, accent, background, surface, text, muted, success/warn/error, focus, hover/active/disabled). Enforce WCAG 2.1 AA contrast for text and interactive states, offering smart adjustments that preserve brand hue while increasing contrast where needed. Produce light and dark variants, color-blind–safe options, and map tokens to ClassTap components (calendar, class card, buttons, forms, modals, checkout). Expose tokens as a versioned JSON schema consumable by web, widget, and email templates.

Acceptance Criteria
Auto-Generate Accessible Theme from Extracted Brand Inputs
Given extracted brand color and typography inputs When the user generates a theme Then a theme is produced in ≤ 5 seconds without error And all text vs background color pairs meet WCAG 2.1 AA (normal text ≥ 4.5:1, large text ≥ 3:1) And non-text UI elements meet WCAG 2.1 Non-Text Contrast ≥ 3:1 against adjacent colors And a compliance summary lists any tokens adjusted for contrast
Semantic Token Set Completeness for Light and Dark Variants
Given theme generation completes When tokens are emitted Then the following semantic tokens exist for both light and dark: primary, secondary, accent, background, surface, text, muted, success, warn, error, focus, hover, active, disabled And each token value is a valid 6-digit hex (#RRGGBB) And hover, active, and disabled tokens are not identical to their base state tokens And zero required tokens are missing
Hue-Preserving Smart Contrast Adjustments
Given any brand-derived token fails WCAG thresholds When smart adjustments are applied Then the adjusted token achieves required contrast And the hue shift is ≤ 10° compared to the original And the perceptual color difference ΔE00 is ≤ 8 And the token is flagged with adjusted=true and reason="contrast" in metadata
Color‑Blind–Safe Variant Generation
Given Color-Blind–Safe mode is enabled When status tokens are generated Then success, warn, and error tokens are pairwise distinguishable with CIEDE2000 ΔE ≥ 10 under protanopia, deuteranopia, and tritanopia simulations And each status-on-background text pair meets WCAG 2.1 AA (normal ≥ 4.5:1, large ≥ 3:1) And the color-blind–safe variant is available alongside light and dark in the export
Interactive States and Focus Visibility Compliance
Given tokenized components are rendered When hover, active, focus, and disabled states are displayed Then hover and active states have ≥ 3:1 contrast against both their default state and the background And the focus indicator using the focus token has ≥ 3:1 contrast against adjacent colors And disabled state visuals are clearly distinct with a luminance difference ΔL* ≥ 20 from the enabled state
Component-Level Token Mapping and Live Preview
Given generated tokens are applied to previews When rendering calendar, class card, buttons, forms, modals, and checkout Then 100% of color styles resolve from semantic tokens (no hardcoded color literals) And changing any single token updates all affected preview components within 300 ms And all previewed components pass automated WCAG AA checks in both light and dark variants And any missing mapping triggers a visible error with the component name and token key
Versioned Token JSON Schema for Web, Widget, and Email
Given a theme is ready for export When tokens are exported Then a JSON document is produced with schemaVersion (semver), themeId, themeVersion (semver), variants [light, dark, colorBlindSafe], tokens, and componentMappings And the JSON validates against the published $schema URL And color values are provided as hex and OKLCH objects for each token And MAJOR/MINOR/PATCH are incremented according to breaking/additive/patch changes And the export is consumable by reference implementations for web widget and email templates without parse errors
Real-time Theme Preview Sandbox
"As a solo instructor, I want to see changes reflected immediately across my booking pages so that I can confidently finalize a theme without publishing it to customers yet."
Description

Provide a live, interactive preview of key booking flow screens and components (schedule list, instructor profile, class detail, cart/checkout, success screens) that updates instantly as users tweak colors, fonts, and image treatments. Include device presets (mobile/tablet/desktop), light/dark toggles, and sample content states (empty, full, error). Target <100ms perceptual latency for style updates via virtualized component playground and CSS variables. Enable snapshot comparison (before/after) and shareable preview links for stakeholders.

Acceptance Criteria
Instant Style Update Under 100ms
Given the preview sandbox is loaded and a key screen is visible When the user commits a change to any theme token (color, font, radius, image treatment) Then the visible preview reflects the new styles within 100ms at the 95th percentile, measured from input commit to first paint Given the user drags a color or font control causing ≥10 updates per second for 5 seconds When the preview renders during the drag Then the preview sustains ≥45 FPS at the 95th percentile and records no main-thread tasks >50ms during the interval Given style updates are applied repeatedly When observing component lifecycle instrumentation Then no unmount/mount events are emitted for preview components; updates occur via CSS variables only
Device Presets Responsive Preview
Given the device preset selector offers Mobile, Tablet, and Desktop When Mobile is selected Then the preview viewport is set to 375x812 and the layout matches mobile breakpoints without horizontal scroll When Tablet is selected Then the preview viewport is set to 768x1024 and the layout adapts to tablet breakpoints When Desktop is selected Then the preview viewport is set to 1440x900 and the layout adapts to desktop breakpoints Then switching presets preserves the current theme settings and visible screen
Light/Dark Mode Toggle Consistency
Given any screen is visible in the preview When the user toggles Light ↔ Dark mode Then background, text, buttons, and image treatments update consistently within 100ms P95 and remain legible with no collision of tokens Then mode selection persists across navigation and device preset changes Then reverting the toggle restores the prior mode without residual artifacts (no mixed-mode styles)
Sample Content States: Empty, Full, Error
Given the Content State control offers Empty, Full, and Error When Empty is selected on each screen Then schedule shows a “No classes available” state, instructor profile shows skeleton/placeholder, cart shows empty cart message, and success screen remains accessible When Full is selected on each screen Then schedule lists at least 20 items with pagination or virtualization active, instructor profile renders bio + class list, class detail shows variants including one sold-out, cart shows line items with totals, success displays confirmation metadata When Error is selected on each screen Then an error banner/toast is shown with a retry action and the preview remains interactive without crashing
Snapshot Before/After Comparison
Given a theme configuration is applied When the user clicks Capture Snapshot Then a timestamped snapshot of the current preview is stored (max 5 snapshots retained; oldest is dropped on overflow) When the user modifies styles and opens Compare Then the user can view Before/After side-by-side and overlay modes and toggle style-diff highlights Then Reset Comparison clears the compare mode and restores normal preview
Shareable Preview Links Persist State
Given the user has a configured preview When Generate Share Link is clicked Then a read-only URL is created that encodes theme tokens, selected screen, device preset, mode (light/dark), and content state When the link is opened in a new session Then the preview loads with the exact saved configuration and editing controls are disabled When the owner revokes the link or it expires after 7 days Then the URL returns 404 and no configuration is disclosed
End-to-End Navigation Across Key Screens
Given the preview navigation lists Schedule, Instructor Profile, Class Detail, Cart/Checkout, and Success When the user clicks each nav item Then the corresponding screen loads with representative mock data within 500ms When the user clicks Book on a class in Schedule or Class Detail Then the preview routes to Cart/Checkout with that class pre-populated When the user clicks Complete Checkout Then the preview routes to Success and retains applied theme styles Then navigating Back preserves the current theme state across screens
Font Pairing and Fallback Management
"As a studio manager, I want suggested font pairings and safe fallbacks so that my booking site typography looks polished and loads fast on all devices."
Description

Recommend font pairings that align with the detected brand style (e.g., geometric sans + humanist sans, serif + sans) using a curated library (Google Fonts) and heuristics (x-height, contrast, mood). Support custom font uploads (WOFF2/WOFF) with automatic subset generation, preloading hints, and licensing acknowledgments. Configure robust fallback stacks and locale-aware font selection. Ensure performance budgets (CLS, FOUT/FOIT mitigations) and map font roles to tokens (heading, body, UI, mono).

Acceptance Criteria
Brand-based font pairing recommendation
Given a brand source (URL or logo) has been analyzed When the user opens Font Pairing recommendations Then 3–5 pairing options from Google Fonts are displayed within 700 ms on a 3G Fast profile And each pairing defines heading, body, UI, and mono roles with specific families and weights And each pairing is tagged with classifications (e.g., geometric sans, humanist sans, serif) aligned to detected brand heuristics (x-height, contrast) And the same brand input yields the same top 3 pairings across sessions for determinism And selecting “Why this pairing?” shows at least one heuristic-based rationale per font
Apply selected font pairing to design tokens
Given a user selects a recommended font pairing When the user clicks Save Then design tokens for heading, body, UI, and mono are updated and applied across the previewed components (headings, paragraphs, buttons, inputs, tables) immediately And only required weights/styles are included in @font-face declarations And the configuration persists across sessions and is used on the published booking pages And total compressed transfer size for initial route font files is ≤ 200 KB for Latin-only locales
Custom font upload with automatic subsetting and license acknowledgment
Given the user uploads font files When files are WOFF2 or WOFF Then the upload succeeds, family/weight/style are detected, and roles can be mapped And a license file is auto-detected or the user must acknowledge a license before enabling the font And automatic subsets are generated per selected locales (Latin default), reducing size by ≥ 30% when applicable And invalid formats (e.g., TTF/OTF) are rejected with a clear message and guidance And preload/preconnect hints are added for first-paint fonts
Font loading performance and FOIT/FOUT mitigation
Given a configured theme is published When the booking page loads on a cold-cache 3G Fast profile Then CLS attributable to font loading is < 0.01 post-first paint And FOIT duration is ≤ 100 ms (font-display prevents invisible text beyond that) And text becomes visible within 1.2 s (first text render) And no more than 2 critical font files are preloaded for the initial route And no more than 1 reflow occurs due to font swap
Fallback stacks and locale-aware font selection
Given a user’s locale is detected When rendering UI text and any glyph is missing in the primary font Then a locale-appropriate fallback stack is used to ensure 100% glyph coverage with no tofu And fallback selection for non-Latin scripts (e.g., CJK, Cyrillic) prefers system fonts or Google fallbacks suited to the locale And layout shift from fallback substitution is < 0.01 CLS And Latin-only custom uploads automatically defer non-Latin scripts to the configured locale fallback
Live preview updates for font changes
Given the design preview is open When the user changes the selected pairing or uploads a new font and maps roles Then the preview updates within 200 ms without a full page reload And all preview components reflect the new tokens consistently And Undo restores the prior configuration and cancels further font requests for removed families
Error handling and rollback for font failures
Given an active font file fails to load due to 404, CORS, or corruption When the page renders Then the next font in the fallback stack is applied within 100 ms and text remains readable And an admin-visible log entry is created with error type and request ID And publishing is blocked for themes with enabled custom fonts lacking license acknowledgment
Brand-aligned Image Treatment Presets
"As an instructor, I want simple image style presets that match my brand colors so that my class photos look consistent without hiring a designer."
Description

Offer optional image treatments that harmonize photos with the generated palette: brand-tinted overlays, duotone filters, vignette/soften, corner radius and shadow tokens, and contrast-aware text overlays. Provide intensity sliders with real-time preview and ensure text-on-image meets contrast guidelines. Process transformations on demand via CDN-compatible URLs with caching and automatic WebP/AVIF output for performance.

Acceptance Criteria
Real-time brand overlay with intensity slider
- Given a generated brand palette, when the user enables Brand Overlay and adjusts intensity (0–100), then the image preview updates within 200 ms p95 to reflect the correct palette color tint. - Intensity 0 disables the overlay; 100 applies full tint; intermediate values map linearly to opacity within ±5% tolerance. - The default overlay color is Primary 500; user can switch to Secondary 500; the selected color and intensity persist after Save and are applied on published pages. - Undo/Redo restores prior values without preview desynchronization. - Exported/published images render the same result as the preview (ΔE00 color difference ≤ 2 vs. preview).
Duotone filter aligned to brand palette
- Given a base image, when the user selects a Duotone preset (Primary–Secondary, Primary–Neutral, High-Contrast Brand) and adjusts intensity (0–100), then the live preview updates within 200 ms p95. - Duotone colors are derived from the generated palette tokens; measured hue/brand mapping deviation ≤ ΔE00 3 from the token colors. - Intensity 0 returns the original image; 100 applies the full duotone mapping; intermediate values blend linearly within ±5% tolerance. - Saved settings are deterministic and reproduced via CDN URL parameters (e.g., fx=duotone&c1=primary500&c2=secondary500&intensity=70). - Visual regression tests across 10 sample images show no banding or posterization artifacts above threshold (PSNR ≥ 35 dB).
Vignette and soften effects with preview and persistence
- When the user enables Vignette and/or Soften and adjusts intensity sliders (0–100), the preview updates within 200 ms p95 and matches saved output (SSIM ≥ 0.98 vs. preview snapshot). - Vignette center defaults to image center with feathering; intensity 0 disables effect; 100 applies maximum effect within defined bounds; Soften kernel scales predictably with intensity. - Effects compose correctly with overlay/duotone (commutative ordering as specified) and produce deterministic output via CDN params (e.g., fx=vignette:int=40,soften:int=20). - Disabling an effect removes it from the CDN URL and from the rendered output on published pages. - No visible haloing at image edges (edge artifact width ≤ 2 px on test set).
Corner radius and shadow tokens applied consistently
- User can select radius tokens (none, sm, md, lg, pill) and shadow tokens (none, sm, md, lg); changes reflect in preview across all image components within 200 ms p95. - The same tokens propagate to booking UI components that display images (cards, tiles, hero blocks) with consistent values across breakpoints (variance ≤ 1 px). - Token values are emitted as design tokens/CSS custom properties and persist after Save; published pages reflect the same tokens. - Shadow rendering meets accessibility contrast guidance for focus/hover states on images (non-text contrast ≥ 3:1 against adjacent background). - Removing tokens reverts components to default theme values without residual styling.
Contrast-aware text over image meets WCAG
- For text rendered over images, the system automatically evaluates contrast on the composed background and ensures WCAG 2.1 AA: ≥ 4.5:1 for normal text and ≥ 3:1 for large text (≥ 24 px regular or ≥ 18.66 px bold). - If contrast fails, an automatic remedy (gradient overlay or backplate using brand-neutral/primary variants) is applied to achieve compliance while preserving brand palette; success is re-validated post-application. - The algorithm selects the text color (light/dark brand token) that yields the highest valid contrast before applying remedial overlays. - A real-time indicator displays Pass/Fail in the editor within 200 ms p95 of any change; publish is blocked if AA is not met and “Enforce contrast” is enabled. - Published output matches editor result; spot checks on 10 sample images confirm ratios meet or exceed thresholds (tolerance −0/+0.1).
CDN on-demand image transformations with caching
- Transforms are expressed via CDN-compatible URLs (path or query params) covering overlay, duotone, vignette, soften, radius, and shadow; invalid params respond 400 with error code. - First-generation latency for a 1600px longest-edge transform is ≤ 350 ms p95; cached hits at the edge are served ≤ 60 ms p95. - Responses include Cache-Control: public, max-age≥31536000, immutable; ETag/Last-Modified are set; If-None-Match yields 304 for unchanged content. - Automatic cache-busting is supported via a required version parameter (v=yyyyMMdd or semantic), and changing v invalidates the cached variant. - URLs include Vary: Accept for content negotiation and are deterministic/idempotent for identical parameter sets.
Automatic AVIF/WebP output with quality safeguards
- When the client sends Accept: image/avif,image/webp,*/*, the CDN serves AVIF if supported, else WebP, else original JPEG/PNG; Content-Type reflects the chosen format and Vary: Accept is set. - Visual quality is maintained with SSIM ≥ 0.98 vs. source (post-transform) and no chroma subsampling artifacts on test images; if target cannot meet SSIM at configured quality, fall back to the next format. - Byte-size reduction targets: AVIF ≥ 30% and WebP ≥ 20% vs. equivalent JPEG/PNG baselines on the test set; if savings < 5%, serve source format to avoid regressions. - ICC color profile is preserved or converted to sRGB with proper tagging; EXIF is stripped except orientation, which is respected in output. - Format choice and resulting size are logged/observable for performance monitoring.
One-click Apply with Versioning & Rollback
"As a studio owner, I want to apply my theme everywhere at once and be able to revert if needed so that I can safely launch changes without risking my live booking flow."
Description

Apply the selected theme across all ClassTap touchpoints (hosted booking pages, embeddable widgets, emails) with one action. Create immutable versioned snapshots of design tokens and assets, store audit metadata (who/when), and allow instant rollback to any prior version. Propagate changes via configuration service with safe rollout (canary) and automatic cache invalidation for CDN assets to ensure consistent customer experience.

Acceptance Criteria
One-Click Apply Across All Touchpoints
Given a saved theme is selected as the target And the user has permission to manage branding When the user clicks “Apply Theme” once Then the active theme version is updated And hosted booking pages render the new theme within 60 seconds of confirmation And embeddable widgets render the new theme on next load within 60 seconds And all emails generated after the apply event use the new theme And the UI displays a success state with the new active version ID And fewer than 0.1% of requests serve mixed-theme assets in the first 2 minutes
Version Snapshot Creation, Immutability, and Audit Trail
Given a theme apply operation completes Then a new immutable version snapshot is created with a unique, monotonically increasing version ID And the snapshot stores the complete set of design tokens and asset digests (content hashes) And audit metadata includes actor ID, actor display name, UTC timestamp, client/IP, and action type “apply” And prior versions remain readable and cannot be edited or deleted by non-system actors And any attempt to modify a prior version is rejected with HTTP 409 (Conflict) and no data change And the version history endpoint/view lists the new version first with correct metadata
Instant Rollback to Any Prior Version
Given at least two theme versions exist And the user has permission to manage branding When the user selects a prior version and confirms “Rollback” Then the selected version becomes the active version globally within 60 seconds And a new version entry is created representing the rollback action referencing the prior snapshot And all touchpoints (pages, widgets, emails) render the rolled-back theme on next load/send And no user session experiences mixed-theme UI during a single session And an audit log entry is recorded with actor, timestamp, target version, and action type “rollback”
Safe Canary Rollout with Abort and Auto-Rollback
Given canary rollout stages are configured (e.g., 5% → 25% → 100%) When the apply operation starts Then only the specified percentage of end-user sessions receive the new theme per stage with session stickiness And promotion to the next stage requires explicit user confirmation And if theme render error rate exceeds 1% for 5 consecutive minutes during any stage, the system automatically aborts and reverts to the last stable version within 2 minutes And when the user clicks “Abort,” the system reverts within 2 minutes And rollout metrics (cohort %, error rate, latency) are visible to the user during canary
CDN Cache Invalidation and Asset Versioning Consistency
Given a new theme version is activated Then all CSS/JS/image asset URLs are versioned or content-hashed to prevent cache collisions And CDN purge or cache-busting ensures 99% of edge locations serve the new assets within 2 minutes and 100% within 10 minutes And no 404s are observed for referenced assets during rollout And fewer than 0.1% of requests exhibit asset-version mismatch (HTML vs. asset) within the first 10 minutes And old assets remain retrievable for 24 hours to support in-flight sessions
Atomic Apply, Concurrency Control, and Failure Handling
Given two apply/rollback operations are initiated within the same minute Then the system serializes by version preconditions and rejects the second with a clear conflict message unless retried against the latest head And apply operations are atomic: either all touchpoints are updated or none are updated And on any deployment or propagation failure, the system automatically reverts to the last stable version and marks the attempt as failed in the audit log And a failed apply does not change the active version ID And the UI surfaces clear success/failure status, timestamp, and version IDs to the user
Theme Export/Import and Sharing
"As a multi-location studio operator, I want to export and import my theme so that I can reuse consistent branding across multiple accounts quickly."
Description

Enable export of the finalized theme as a portable JSON of design tokens (colors, typography, radii, shadows, spacing) and image treatment presets. Support import from the same JSON or Figma Tokens format to replicate branding across locations or accounts. Provide read-only share links for collaborators to preview without login and a copy-as-new workflow to accelerate multi-brand setups.

Acceptance Criteria
Export Design Tokens JSON
Given a finalized theme in BrandSnap Palette When the user selects Export -> ClassTap JSON Then a file named "<theme-slug>-tokens-v<semver>.json" is downloaded And the JSON validates against the ClassTapThemeTokens schema for the current major version And it contains sections: metadata, tokens.colors, tokens.typography, tokens.radii, tokens.shadows, tokens.spacing, imageTreatments And sizes are serialized with units (e.g., "16px", "1.25rem"), angles in "deg", and colors as #RRGGBB with optional alpha field 0–1 And the export excludes any user PII and secrets And the export completes within 2 seconds for themes with ≤ 500 tokens and ≤ 10 image treatments And the file size is ≤ 500 KB for the constraints above
Import ClassTap JSON Theme
Given a valid ClassTap Theme JSON produced by Export When the user imports the file Then a new theme is created without modifying the currently selected theme And all token values and image treatments match the source after normalization And the preview renders using the imported tokens (CSS variables equal to source) And missing fonts fall back to the configured default with a non-blocking warning And on schema validation failure, the import is aborted with an error including the first failing JSON path And no partial data is saved on failure (atomic) And the operation completes within 5 seconds for files ≤ 500 KB
Import Figma Tokens JSON
Given a valid Figma Tokens JSON file When the user imports it using Import -> Figma Tokens Then color, typography, spacing, radius, and shadow tokens are mapped to ClassTap equivalents per documented mapping And unmapped or unknown tokens are summarized and ignored without blocking the import And image treatment presets are created only if provided under extensions.imageTreatments; otherwise defaults are used And required minimal tokens (primary color, background, text base, font family, base spacing) are present post-import or the user is prompted to fill them before saving And schema or parse errors show a clear message and abort the import with no partial save And the operation completes within 5 seconds for files ≤ 1 MB
Read-Only Share Link Preview
Given an approved theme When the user clicks Share -> Generate read-only link Then an HTTPS URL with an unguessable 128-bit random token is created And the default expiry is 7 days, with options for 1 hour, 24 hours, 7 days, and 30 days And visiting the link shows an interactive preview (desktop/mobile switch) with no edit or save controls and no login required And theme token values are loaded read-only; no personal data is exposed And the owner can revoke the link at any time; revoked or expired links return HTTP 410/404 And view count and last-viewed timestamp are recorded and shown to the owner And the link access is rate-limited to 60 requests per minute per IP
Copy-as-New From Shared Theme
Given a user is authenticated and has access to BrandSnap Palette And they open a valid read-only share link or an existing theme When they choose Copy as New Then a new theme is created in their account with a new ID, preserving all tokens and image treatment presets And the new theme is titled "Copy of {Original Name}" (appending a numeric suffix if the name already exists) And the source account gains no access to the new theme And an audit log entry is recorded with source theme ID, destination account, actor, and timestamp And the operation completes within 3 seconds for themes ≤ 500 tokens and ≤ 10 image treatments
Versioning and Backward Compatibility
Given export and import occur across app versions When exporting a theme Then the JSON includes metadata.version in SemVer (e.g., "1.2.0") and metadata.schemaMajor And when importing, files with the current major and up to two previous minor versions are accepted without prompts And files with a lower major version are auto-migrated with a migration report shown before saving And files with a higher major version are rejected with a clear message and link to documentation And unknown fields are preserved under metadata.extensions for round-trip compatibility

Pricing Translator

Automatically converts passes, packs, memberships, taxes, and discounts from your old system into ClassTap equivalents. Get side‑by‑side comparisons and guidance to preserve revenue rules, minimize price drift, and launch without confusing clients.

Requirements

Unified Source Import Connectors
"As an instructor migrating to ClassTap, I want to import my existing passes, packs, memberships, taxes, and discounts from my old system so that I don’t have to rebuild everything manually and risk errors."
Description

Ingests pricing artifacts from legacy systems (passes, packs, memberships, taxes, discounts) via CSV upload, direct API, or manual entry. Performs schema detection, guided field mapping, and validation, then normalizes data into ClassTap’s canonical pricing model (product types, durations, usage limits, renewal rules, freeze/cancel policies, tax codes, discount logic). Supports multi-currency and timezone handling, deduplication, and idempotent import sessions with resumable progress, rate limiting, and audit trails. Ensures PII-safe handling and prepares clean inputs for downstream mapping and simulation components.

Acceptance Criteria
CSV Upload with Schema Detection and Guided Field Mapping
Given a CSV file exported from a legacy system containing passes, packs, memberships, taxes, and discounts with varied column naming When the user uploads the file and selects the source system (optional) Then the importer infers artifact types per row and proposes a schema mapping for product type, durations, usage limits, renewal rules, freeze/cancel policies, tax codes, and discount logic And unmapped or ambiguous columns are explicitly flagged and must be resolved before proceeding And a preview of at least 50 sample rows displays mapped fields, normalization outcomes, and validation flags And the user can save the final mapping as a reusable template tied to the source And progression to import is blocked until critical validation errors are 0 And normalized canonical records are persisted with an import batch ID and are made available to downstream mapping/simulation components
Direct API Import with Idempotent Sessions and Rate Limiting
Given an API connector is configured with valid OAuth credentials and an idempotency key When the importer pulls artifacts via paginated endpoints Then provider rate limits are honored by exponential backoff on HTTP 429 and adherence to Retry-After headers And on transient network failure the session resumes from the last successful page without duplicating work And re-running within the idempotency window with the same key does not create duplicates or unintended updates And a checksum of source payloads is stored to detect changes and apply upserts only when data differs And normalized canonical artifacts are committed atomically per batch and flagged for downstream mapping/simulation
Manual Entry of Pricing Artifacts with Real-time Validation
Given a user chooses Manual Entry for a new pricing artifact When they input price, ISO 4217 currency, term, usage limit, renewal rules, freeze/cancel policies, tax code, and discount logic Then inline validation enforces required fields, allowed ranges, formats, and selection of a valid timezone where applicable And a normalized preview of the canonical model is shown before save And on save exactly one canonical record is created and becomes available to downstream mapping/simulation components And the action is audit-logged with actor, timestamp, and field-level diff
Multi-Currency and Timezone Normalization
Given source data includes multiple currencies and timezones across pricing and schedule-related fields When the data is imported Then monetary values are stored with amount and ISO 4217 currency code without implicit conversion And all time-based fields are normalized to UTC while preserving original tz database identifiers And renewal/proration dates reconstruct to the same local wall time from the canonical model And imports fail validation for unrecognized currency or timezone codes with clear error messages
Cross-Source Deduplication Across Sessions
Given identical business artifacts may appear from CSV and API with different source IDs When an import session runs Then a deterministic business-key hash (e.g., normalized name, SKU, duration, price, core rules) is used to detect duplicates And duplicates are merged without creating additional records, keeping the most recent source-of-truth per timestamp policy And the audit trail lists merged source IDs and winning record details And the session summary reports counts of detected and merged duplicates
Resumable Import Sessions with Progress and Audit Trail
Given an import session is in progress (CSV or API) When the browser is refreshed, the user pauses, or a timeout occurs Then the session can be resumed from the last committed step with no data loss And progress shows percent complete plus counts of processed, valid, invalid, deduplicated, and committed records And a signed, exportable audit log includes session ID, actor, steps, validations, dedupe merges, upserts, timestamps, and excludes PII And re-running a completed session with the same idempotency key defaults to a dry-run preview until explicitly confirmed to apply changes
PII-Safe Handling and Redaction
Given source files or API payloads may include PII (names, emails, phone) unrelated to pricing When ingestion and validation run Then PII fields are ignored or tokenized and never written to canonical pricing storage And PII is redacted from logs, previews, error messages, and audit records And automated checks prevent code paths that persist PII in pricing import components And security tests verify exported audit artifacts contain no PII
Product Mapping Rule Engine
"As a studio owner, I want flexible mapping rules between my old plans and ClassTap products so that the business logic behind my pricing is preserved."
Description

Translates legacy products into ClassTap equivalents using configurable, reusable mapping rules. Supports mapping for class access scopes, credit counts, validity windows, auto-renewal and cancellation policies, proration, freeze/hold options, family sharing, attendance limits, and blackout periods. Includes rule precedence and conflict resolution, template libraries for common legacy systems, and preview of proposed mappings. Ensures preservation of revenue rules and business logic while conforming to ClassTap’s product model.

Acceptance Criteria
Access Scope, Attendance Limits, and Blackout Periods Mapping
Given a legacy product "Gold Pack" with access categories ["Yoga","Pilates"], excluded classes ["Hot Yoga"], locations ["Main","Annex"], a daily attendance limit of 2, and blackout dates from 2025-12-24 to 2025-12-26 When the rule engine maps the product using the "Generic-Access-and-Limits" rules Then the mapped ClassTap product grants access only to Yoga and Pilates at Main and Annex And excludes Hot Yoga classes And enforces a maximum of 2 check-ins per purchaser per calendar day And enforces a blackout from 2025-12-24 through 2025-12-26 inclusive
Credit Counts and Validity Windows Translation
Given a legacy pass "10-Class Card" with credit_count=10, validity=90 days from purchase, and expiration behavior set to forfeit remaining credits at expiry When the rule engine maps the pass to ClassTap Then the ClassTap product is configured with 10 credits And a validity window of 90 days starting at purchase timestamp And remaining credits are not usable after expiry And if any attribute (e.g., grace_period) is unsupported, a variance entry is created with attribute name, mapping status="Not Mapped", and suggested mitigation
Auto-Renewal, Cancellation Notice, and Proration Policies
Given a legacy membership "Monthly Unlimited" with auto_renews=true, interval=1 month, cancellation_notice=14 days, proration model=align to 1st of month with proportional charge, and refund policy=prorate unused days on early cancel When the rule engine maps the membership using the "Membership-AR" rules Then the ClassTap membership auto-renews monthly And cancellation cannot be scheduled within the next 14 days without triggering the defined fee/block And mid-cycle signups are prorated to align with the 1st of the next month with charge parity within ±$0.01 for a $100/month price And early cancellation refunds unused days with refund parity within ±$0.01 for a cancel 10 days before renewal And the mapping preview reports price drift for proration scenarios <= 0.1%
Freeze/Hold Options and Fees Mapping
Given a legacy membership that allows up to 2 freezes per rolling 12 months, each freeze duration between 7 and 30 days, with a freeze fee of $10 per freeze, and billing extended by total frozen days When the rule engine maps the membership Then the ClassTap membership allows a maximum of 2 freezes per rolling 12 months And enforces minimum freeze duration of 7 days and maximum of 30 days And applies a $10 fee per approved freeze And extends the next bill date by the total number of frozen days And if the exact freeze cadence cannot be replicated, the engine records a variance with expected revenue delta estimate and an approximation rule applied
Family Sharing and Transferability Settings
Given a legacy pack that enables family sharing with up to 3 designated members, credits decrement per attendee across owner and sharees, only the owner may invite/remove sharees, and transfers of ownership are not allowed When the rule engine maps the pack Then the ClassTap product enables family sharing for up to 3 sharees And decrements credits per attendee usage across owner and sharees And restricts invite/remove permissions to the owner only And disallows product ownership transfers And enforces daily attendance limits at the purchaser account level
Rule Precedence and Conflict Resolution Logging
Given two matching rules: Template "Generic Punchcard" and Org Override "Pilates Packs" with precedence set to Org Override > Template, conflicting on validity_window (60 vs 90 days) and daily_attendance_limit (1 vs 2) When the rule engine evaluates mappings Then the engine applies the higher precedence values (validity_window=90 days, daily_attendance_limit=2) And produces a conflict resolution log capturing rule IDs, fields overridden, old and new values, timestamp, and actor And the resulting mapping contains no unresolved conflicts or ambiguity flags
Template Application and Mapping Preview with Revenue Drift Guardrail
Given the source system is "MindBody" and the user selects the template "MindBody v2 Punchcard" for a legacy product priced at $150 When the user applies the template and opens the mapping preview Then the preview displays a side-by-side comparison of all mapped fields with highlighted differences And shows an estimated monthly revenue variance at a cohort size of 100 customers And if the variance exceeds 0.5%, the system blocks finalize and requires acknowledgment or rule adjustment And Finalize Mapping is enabled only when variance <= 0.5% or the user provides an explicit override with a reason
Tax and Discount Conversion with Drift Controls
"As an admin, I want my taxes and discounts to be converted accurately with minimal price drift so that clients see consistent pricing and we stay compliant."
Description

Converts tax configurations and discount structures from legacy systems, handling tax-inclusive/exclusive pricing, jurisdictional tax codes, and rounding behavior. Translates discount types (percent, fixed amount, bundles, BOGO), stacking/exclusion rules, eligibility criteria, usage limits, and expiry. Applies configurable price-drift thresholds and rounding strategies per currency, raising warnings or auto-adjusting to keep deltas within tolerance. Validates compliance with ClassTap’s tax engine and produces a clear audit of converted rules.

Acceptance Criteria
Convert Tax-Inclusive vs Tax-Exclusive with Jurisdiction Codes
Given a legacy item priced 119.00 EUR tax-inclusive with 19% VAT code DE-VAT-STD and bankers rounding, and a price-drift threshold of 0.5% configured When the Pricing Translator converts taxes Then the ClassTap item is created as tax-inclusive with VAT code mapped to CT-DE-VAT-STD, net and tax amounts computed so that gross and tax deltas are each ≤ 0.5% And rounding follows bankers rounding to 2 decimals for EUR And the audit log records source code, mapped code, net/tax computations, rounding, and deltas And if the tax code is unmapped, the item is marked Needs Tax Mapping, is not published, and the audit flags the missing mapping
Enforce Price-Drift Thresholds and Rounding per Currency
Given currency rules USD (2 decimals) with absolute drift threshold $0.10 and rounding mode Half Up, and JPY (0 decimals) with absolute drift threshold ¥1 and rounding mode Round Down When conversion produces a price delta exceeding the configured threshold Then, if Auto-Adjust is enabled, the system adjusts the converted price to the nearest value within threshold and records the adjustment reason in the audit And, if Auto-Adjust is disabled, the system raises a blocking warning, prevents publish, and records the over-threshold delta in the audit And all amounts are rounded using the configured currency-specific strategy
Translate Complex Discount Structures with Stacking Rules
Given legacy discounts of types: Percentage (15%), Fixed Amount ($10), Bundle (5 for $80), and BOGO (Buy 1 Get 1 50% off), with stacking allowed for Percentage+Fixed and excluded with BOGO When the translator converts discounts Then equivalent ClassTap discounts are created reflecting type, values, and stacking/exclusion constraints And application order is preserved (e.g., Percentage before Fixed as configured) And validation using three predefined carts (1 item, 2 items, bundle-eligible set) yields totals within the configured drift threshold vs legacy And the audit includes a mapping matrix of old->new discount IDs and stacking rules
Preserve Discount Eligibility, Usage Limits, and Expiry Windows
Given eligibility rules (new customers only, role=instructor excluded, class tags includes Yoga), min order $50, per-user usage limit=1, global cap=100, start=2025-10-01 00:00 and end=2025-10-31 23:59 in source timezone America/New_York When discounts are converted Then the resulting ClassTap rules enforce the same eligibility, limits, and window (to-the-minute in source timezone) And simulated redemptions confirm per-user lockout after 1 use and stop at 100 global redemptions And attempts before start or after end are rejected And the audit lists any constraints that were relaxed or tightened and why
Validate Tax Codes Against ClassTap Tax Engine
Given a set of legacy jurisdictional tax rules including a compound CA GST+QST and a single-rate US-CA state tax When validation runs during conversion Then supported rules map to ClassTap tax codes and structures with deltas ≤ configured thresholds And unsupported constructs are either translated to supported equivalents marked Approximated with an estimated delta, or conversion is blocked with explicit reasons And the audit provides a compliance verdict per rule with links to the mapped ClassTap tax codes
Preserve Tax-Discount Application Order
Given a legacy configuration where discounts apply pre-tax for EU markets and post-tax for specific US jurisdictions When conversion occurs Then ClassTap pricing flows reflect the same application order per jurisdiction And verification carts for each jurisdiction produce totals matching legacy within threshold And the audit explicitly records the apply-order rule bound to each jurisdiction
Generate Conversion Audit Report with Deltas and Rule Mapping
Given a completed conversion batch containing taxes and discounts When the batch finishes Then a downloadable audit report (CSV and JSON) is produced containing for 100% of items: source identifiers, new identifiers, rule mappings, pre/post amounts, absolute and % deltas, rounding modes, thresholds, warnings/errors, timestamps, and actor And any item with delta > threshold and Auto-Adjust off is labeled Warning and excluded from publish And report generation completes within 60 seconds for up to 5,000 items
Side-by-Side Comparison and Guidance UI
"As an instructor, I want a clear side-by-side comparison of old and new pricing so that I can confidently approve the migration."
Description

Provides an interactive diff view comparing each legacy item to its proposed ClassTap equivalent, highlighting changes in price, benefits, limits, and policies. Surfaces drift indicators, rule mismatches, affected client counts, and projected revenue impact. Offers inline guidance, best-practice tips, and smart suggestions to resolve discrepancies. Includes filters, search, bulk actions, and exportable reports, with an approval workflow to capture sign-off before applying changes.

Acceptance Criteria
Diff View Highlights Core Attribute Changes
Given a legacy item mapped to a proposed ClassTap equivalent When the comparison view loads Then changes in price, benefits, usage limits, and cancellation/no-show policies are visually highlighted And unchanged fields are displayed with a neutral style And hovering a highlighted field shows a tooltip with legacy value, proposed value, and percent change (for numeric fields) And price differences display currency symbols based on workspace settings And the view renders within 1000 ms for up to 2,000 items loaded client-side
Drift Indicators and Revenue Impact Display
Given drift thresholds of ±5% (minor) and ±10% (major) When a proposed item differs from legacy values Then a drift badge is shown with the correct severity based on percent difference And the projected monthly revenue impact is displayed in workspace currency with sign formatting (negative in red, positive in green), rounded to 2 decimals And affected client counts are shown (active, historical) with tooltips explaining the counts And hovering the impact reveals the calculation basis (time window and data source) And if data is insufficient, the UI shows "Insufficient data" with an info tooltip instead of a number
Rule Mismatch Detection and Inline Guidance
Given a rule mismatch (e.g., legacy discount stacking not supported or differing renewal grace periods) When the item is evaluated Then a mismatch banner appears listing the mismatch type and count of impacted rules And inline guidance explains the issue and links to a knowledge base article And a "Preview Suggestion" action shows the proposed resolution diff without applying it And selecting "Apply Suggestion" updates the mapping and recomputes drift and impact within 2000 ms And the change is written to the change log with user, timestamp, before/after values
Filters and Search Across Items
Given a dataset of up to 10,000 items server-side (2,000 client-visible) When the user applies filters (item type, drift level, mismatch present, impact range, approval status) and free-text search (name, code, description) Then results reflect the combined criteria and update within 500 ms for client-side operations And the UI displays the active filter chips and a result count And "Clear all" removes all filters and restores the full result set And pinned filters persist across sessions for the same user and workspace And no filter returns are paginated consistently with accurate counts
Bulk Actions on Selected Items
Given one or more items are selected When the user triggers a bulk action (Approve, Request Changes, Ignore Suggestion, Apply Suggestion) Then the action executes across all selected items and shows a summary of successes and failures And partial failures list item identifiers and error reasons And destructive bulk actions require confirmation with the exact count of affected items And an Undo option is available for 5 minutes or until a conflicting action occurs And all bulk operations are recorded with a batch ID in the audit log
Exportable Reports
Given an applied filter/search context When the user exports Then CSV and PDF downloads are available containing: legacy ID, legacy name, ClassTap ID (if mapped), change flags, old/new price, old/new benefits, limits, policy diffs, drift %, projected revenue impact, affected client counts, approval status, approver, timestamp And the export respects current filters and sort order And currency and time zone reflect workspace settings in both formats And exports complete within 60 seconds for up to 10,000 items And the number of rows in the export matches the UI count within ±0 discrepancy
Approval Workflow and Audit Trail
Given a user with the Approver role When attempting to approve items with unresolved critical mismatches Then the Approve action is disabled and a message lists blockers And approving requires an optional note and records approver, timestamp, and before/after values And approved items are locked from further auto-mapping changes until explicitly Unapproved by an Approver And Unapprove restores editability and adds an audit entry And users without the Approver role cannot see the Approve/Unapprove controls and receive a 403 on direct API calls
Validation, Simulation, and Conflict Detection
"As a studio owner, I want to simulate the impact of my translated pricing before I publish it so that I can catch issues and avoid revenue loss."
Description

Runs dry-run simulations of the translated pricing over a configurable horizon (e.g., 30–90 days) to project renewals, redemptions, and revenue metrics (MRR, ARPU, discount liabilities). Detects conflicts such as over-permissioned access, broken discount stacking, invalid tax mapping, or plan overlaps that could enable double-bookings. Generates actionable remediation suggestions and risk flags prior to publishing. Produces a summary report for stakeholders and links directly to items requiring fixes.

Acceptance Criteria
Dry-Run Simulation Over Configurable Horizon Produces Revenue Projections
- Given a translated pricing dataset and access to Pricing Translator, When the user selects a 30, 60, or 90-day horizon and starts a dry-run, Then the system simulates renewals, redemptions, MRR, ARPU, and discount liabilities per day and in aggregate without altering production data or charging customers. - Then the simulation enforces horizon validation: values outside 30–90 days are rejected with an inline error. - Then outputs include side-by-side baseline vs translated projections with percent and absolute deltas for each metric. - Then results are available in the UI and downloadable as CSV and JSON. - Then for a dataset of up to 10,000 active customers and 100 pricing items, the simulation completes within 5 minutes at the 95th percentile.
Detect Over-Permissioned Access Rules
- Given source-system entitlements and translated ClassTap entitlements for each pass/pack/membership, When the simulation compares access scopes, Then any expansion beyond source (e.g., additional class types, locations, time windows) is flagged as High severity with a diff of added scopes. - Then each flag includes: rule name, impacted product(s), count of affected customers, sample of 5 affected customers, sample of 5 impacted classes/sessions, and a deep link to the plan configuration. - Then remediation suggestions are provided: tighten scope to match source, or create explicit exception rules with justification notes.
Detect Broken Discount Stacking and Combinability Violations
- Given imported discount rules with defined stacking/combinability policies and priorities, When multiple discounts are eligible on the same transaction, Then the simulation applies policies and flags any case where effective discount exceeds allowed stacking or violates priority/exclusivity. - Then each violation includes a sample cart/order showing item lines, applied discounts, expected vs actual discount, revenue delta, and a deep link to the discount configuration. - Then suggested fixes are provided: adjust combinability, set precedence, cap discount value/percent, or restrict scope. - Then publishing is blocked while any High severity stacking violations remain, unless an Admin performs an override with mandatory rationale and confirmation.
Detect Invalid or Mismatched Tax Mapping
- Given tax codes, jurisdictions, and rates from the source system and the ClassTap tax engine, When simulating invoices across customer locations, Then any item with unmapped tax code or mismatched rate (delta > $0.01 or > 0.1%) is flagged with jurisdiction, expected rate, mapped rate, and calculated delta. - Then the flag includes count of impacted invoices, 5 sample invoice IDs, and a deep link to the tax mapping screen. - Then suggested fixes are provided: map missing codes, correct jurisdiction, or update effective dates. - Then totals reconcile within tolerance (≤ $0.01 per line or ≤ 0.1%); outside tolerance triggers Medium or High severity based on magnitude.
Detect Plan Overlaps That Could Enable Double-Bookings
- Given class schedules, capacity constraints, and translated entitlements, When the system analyzes concurrent booking rights across memberships/packs for the same customer, Then any overlap that enables more than 1 simultaneous booking within the same time window is flagged with severity based on capacity risk and frequency. - Then each flag includes count of potential overlap windows in the horizon, number of impacted classes/sessions, estimated risk to capacity utilization, and deep links to the involved plans. - Then suggested fixes include: unify booking scopes, set concurrency limit to 1, add blackout/buffer windows, or prioritize primary plan over add-ons.
Stakeholder Summary Report, Risk Flags, and Publish Gate
- Given a completed simulation run, When the results are generated, Then a stakeholder report is created showing baseline vs translated metrics (MRR, ARPU, renewals, redemptions, discount liabilities), percent/absolute drift, and conflict counts by severity. - Then each flagged item in the report includes an action label (Fix Now, Review, Accept Risk), and a deep link to the exact configuration or migration record requiring changes. - Then the report is exportable as PDF and shareable via a signed link with role-based access that expires in 7 days. - Then publishing is disabled while any High severity conflicts remain; Admins may override with 2FA and mandatory rationale; all overrides are audit-logged with timestamp, user, and diff of changes. - Then any configuration change invalidates prior report versions and prompts re-simulation before publish.
Bulk Apply, Idempotent Migration, and Rollback
"As an admin, I want to apply the new pricing in bulk with the ability to undo changes so that I can launch safely and recover from mistakes."
Description

Applies approved mappings in batches to create or update ClassTap products, taxes, and discounts with idempotency keys, transactional grouping, and partial retry on failure. Maintains a full audit trail and version history, supports scheduled cutovers, and provides a one-click rollback to the previous state if issues arise. Sends role-based notifications, enforces permissions, and ensures zero-downtime launch windows to minimize disruption to bookings and payments.

Acceptance Criteria
Idempotent Batch Apply of Approved Mappings
Given an approved set of mappings and a unique idempotency key K for the batch When the Apply action is triggered one or more times with the same key within the idempotency retention window (>= 7 days) Then each target entity (product, tax, discount) is created or updated at most once And subsequent requests return a success response indicating "idempotency replay" without additional side effects And the response includes batch_id, correlation_id, and idempotency_key And the idempotency key and resulting entity IDs are persisted and queryable for the retention window
Transactional Grouping with Partial Retry
Given mappings are grouped into transaction groups of up to N items (configurable; default 50) When any operation in a group fails validation or persistence Then the entire group is rolled back atomically and no partial writes remain And failed items are placed in a retry queue with exponential backoff (3 attempts: 30s, 2m, 10m) And successfully applied groups are not re-applied on batch retry And the failure report lists mapping_id, group_id, error_code, and next_retry_eta
Audit Trail and Version History
Given any create or update caused by a batch apply or rollback When the operation completes Then an immutable audit record is stored containing actor_id, role, timestamp (UTC), action_type, batch_id, group_id, mapping_id, idempotency_key, previous_values_checksum, new_values_checksum, and entity_version And a new version is appended to the entity’s version history with a pointer to the audit record And audit records are retrievable via API/UI with filters by date range, actor, batch_id, and entity_type within 2 seconds of commit
Scheduled Cutover with Zero Downtime
Given a cutover is scheduled for time T with a launch window W When time T is reached Then pending approved mappings are applied without placing booking or payment APIs into maintenance mode And booking and payment endpoint uptime during W is >= 99.99% and p95 latency increase is < 20% over the prior 1-hour baseline And pricing changes apply only to new bookings/invoices created after T; existing bookings/invoices retain prior pricing And a dry-run impact report is available at least 1 hour before T showing counts of entities to be created/updated/deactivated
One-Click Rollback to Previous State
Given batch B completed within the rollback lookback window (>= 24 hours) and anomalies are detected When an authorized user initiates "Rollback" for batch B Then all entities affected by B are restored to their immediate prior versions atomically within the batch scope And entities created by B are deactivated or deleted per policy, and previously active entities are reactivated And no existing bookings are canceled and no payments are double-charged as a result of rollback And the system emits a completion notification with outcome, duration, and counts, and the audit trail records the rollback with reason_code
Role-Based Permissions and Notifications
Given role-based access control with roles Owner, Ops Admin, Migration Manager, and Editor When a user attempts to approve mappings, schedule cutover, apply, or rollback Then only Owner, Ops Admin, and Migration Manager can perform the action; others see disabled UI and receive HTTP 403 on API And all of these actions generate notifications to Owner and Ops Admin (email and in-app) within 60 seconds containing batch_id, action, actor, counts, start_time, and outcome And notification delivery status (sent, failed, retried) is logged in the audit trail
Post-Apply Reconciliation and Guardrails
Given a batch apply completes When reconciliation runs Then a report compares source vs target counts and price points by entity type, and validates revenue rule equivalence And price drift thresholds are enforced (memberships 0%, passes/packs <= 1%, taxes and discounts 0%); batches exceeding thresholds are flagged and cutover is blocked until an authorized override is recorded And reconciliation results are available via UI/API with batch_id, drift metrics, discrepancy list, and export option (CSV)
Client Communication and Grandfathering Toolkit
"As a studio owner, I want automated client communications and grandfathering options so that my clients aren’t confused and churn is minimized."
Description

Generates segmented client communication packages (email/SMS/in-app) explaining changes, effective dates, and impacts. Supports grandfathering of existing clients to legacy pricing for a configurable period, with automatic coupon issuance or credits when drift exceeds thresholds. Provides localized templates, scheduling, and A/B variants, integrates with CRM for delivery and tracking, and records acknowledgments to reduce confusion and churn at launch.

Acceptance Criteria
Segmented Audience Generation by Pricing Impact
Given client profiles include plan type, locale, region, contact preferences, consent status, and calculated price drift When the user creates segments by plan type, drift bands (<=0%, 0–5%, 5–15%, >15%), locale, and channel Then the system returns mutually exclusive segments with counts, excludes opted-out/invalid contacts, and supports export/sync to CRM with list IDs and timestamps within 5 minutes for ≤100,000 clients Given a client matches multiple segment rules When segmentation runs Then the client appears in exactly one segment using priority: higher drift band > membership > pack > pass
Localized Templates with A/B Variants across Email/SMS/In‑App
Given templates exist in en-US, es-ES, and fr-FR with placeholders {first_name}, {effective_date}, {plan_name}, {price_change}, {grandfather_end_date}, {coupon_code} When the user selects channel (Email, SMS, In‑App) and locale Then rendered content resolves placeholders, formats currency/date per locale, appends link tracking, and falls back to en-US if a locale is missing Given SMS is selected When validating message length Then the system enforces ≤2 segments (≤306 chars GSM-7 or ≤134 chars UCS-2) and includes compliant opt-out text; validation fails if exceeded Given A/B variants A and B with a configurable split (10/90–50/50) When sending to a segment of ≥1,000 recipients Then recipients are randomly assigned by split with per-variant send counts recorded and variant identifier persisted per message
Scheduled Multichannel Delivery via CRM Integration
Given a campaign with selected segments and templates When the user schedules delivery within 09:00–19:00 recipient local time on a chosen date Then messages are queued and delivered within ±5 minutes of the window per recipient time zone and channel rate limits are respected Given CRM APIs are used for delivery When requests are sent Then 95th percentile delivery attempts complete within 1 hour; failed sends retry up to 3 times with backoff (2m, 10m, 30m) and move to a dead-letter queue with error detail after final failure Given tracking webhooks are configured When delivery/open/click/bounce/unsubscribe events arrive Then events are recorded within 2 minutes with message ID and client ID and are visible in the campaign activity log
Grandfathering Eligibility and Effective Dates
Given a grandfathering policy with cutoff date, duration (3–12 months), and eligible plan types is configured When eligibility is calculated Then clients with active memberships/packs purchased before the cutoff are marked eligible with entitlement records (start, end date, legacy price); others are excluded Given a grandfathered client renews or books within the entitlement window When billing is applied Then legacy pricing is used and revenue rules preserved; after the end date, new pricing applies automatically without manual intervention Given other discounts or coupons exist When checkout evaluates discounts Then grandfathered pricing does not stack with promotional discounts; tax rules apply after price; conflicts are blocked with a clear message
Automatic Coupon/Credit Issuance for Price Drift
Given a drift threshold configured as max(5%, $5) When a client’s new price exceeds legacy price by more than the threshold and the client is not grandfathered for the transaction Then a unique coupon or account credit is issued to limit the first renewal/next purchase increase to ≤ threshold; coupon expires in 30 days (one-time) or applies automatically at next renewal; uniqueness is enforced Given a coupon/credit is issued When composing notifications Then {coupon_code} and value are injected into all channel templates and issuance is logged with timestamp, actor, and reason in the client audit trail Given checkout occurs with an issued coupon When applying discounts Then the coupon is single-use, non-stackable with other promos (tax rules unaffected), and rounding is to nearest $0.01 per locale
Client Acknowledgment Recording and Access Gating
Given a campaign requires acknowledgment When a client opens the email link or in‑app modal Then the UI displays changes, effective dates, and detail links with WCAG 2.1 AA compliance and provides Accept and Remind Me Later actions Given the client has not acknowledged after announcement When attempting to book or renew on/after the effective date Then checkout is blocked with a clear message until acknowledgment; Remind Me Later allowed up to 3 times before gating applies Given the client accepts When recording acknowledgment Then timestamp, channel, message version, and device/IP are stored idempotently and are viewable/exportable by admins
Launch Readiness and Post‑Send Reporting
Given segments and templates are selected When running pre‑send simulation Then the system returns per‑channel recipient counts, locale coverage, estimated SMS segments and cost, % of clients above drift threshold, and missing template warnings within ≤2 minutes for ≤100,000 recipients Given a live campaign has run for ≥24 hours with ≥500 deliveries per A/B variant When viewing the dashboard Then deliveries, opens, clicks, bounces, unsubscribes, and acknowledgment rates are available by segment and variant; the variant with ≥1pp higher click‑through rate is flagged as the winner Given an export is requested When generating CSV/API output Then event‑level data (message ID, client ID, timestamps, metrics) is available within 5 minutes for download or API retrieval

Policy Port

Import and standardize cancellation windows, no‑show rules, waivers, and terms into reusable ClassTap policy templates. Auto‑attach them to classes and programs so your compliance and customer expectations carry over seamlessly.

Requirements

Policy Template Builder
"As a studio owner, I want to create reusable policy templates so that I can consistently apply the same rules across all my classes and programs without rewriting them."
Description

Provide a structured template system to define cancellation windows, no‑show rules, waivers, and terms as reusable, brandable assets. Support rich text, variable tokens (e.g., {class_name}, {start_time}), required fields, categories (cancellation, no‑show, waiver, terms), and effective dates. Enable preview and validation, accessibility-compliant editing, multi-language content, and organization-wide sharing. Templates integrate with ClassTap classes and programs, powering checkout disclosures, enforcement logic, and communications while ensuring consistent application across offerings.

Acceptance Criteria
Create and Save Policy Template (Category, Required Fields, Effective Dates)
Given I have template creation permission, When I open the Policy Template Builder, Then I can start a new template with fields: Name, Category, Content, Effective Start Date, Effective End Date (optional) Given a new template, When Category is selected, Then Save remains disabled until Name, Category, Content, and Effective Start Date are provided Given an Effective End Date earlier than Effective Start Date, When I attempt to save, Then I see an inline error "End date must be after start date" and the template is not saved Given Category is Cancellation, When I attempt to save without a Cancellation Window (hours), Then validation blocks save and highlights the missing field Given Category is No‑Show, When I attempt to save without a No‑Show Action (e.g., fee or credit deduction), Then validation blocks save and highlights the missing field Given all required data is valid, When I click Save, Then the template is persisted with a unique ID and appears in the Policy Templates list within 2 seconds
Token Support and Contextual Preview Rendering
Given the content includes supported tokens {class_name} and {start_time}, When I click Preview and select a class context, Then the tokens render with the selected class's values using locale-appropriate formatting Given the content includes an unsupported token {foo}, When I attempt to save, Then validation lists the unsupported token(s) and blocks save Given I open the token inserter, When I search for "start", Then I can insert from a list of supported tokens with descriptions Given the selected preview context lacks a value for a token, When I preview, Then the token renders as an em dash (—) and an info message indicates missing data
Rich Text Editing with Accessibility Compliance
Given the editor is focused, When I use only the keyboard (Tab, Shift+Tab, Arrow keys), Then I can navigate all toolbar controls and the editing area without traps Given a screen reader is active, When I navigate the toolbar, Then each control exposes an accessible name and state via ARIA and is announced correctly Given the default theme, When I inspect toolbar icons, Then color contrast is at least 4.5:1 against the background Given I format content with headings, bold/italic, ordered/unordered lists, and links, When I save and reopen, Then the formatting is preserved exactly Given I make an edit, When I press Ctrl/Cmd+Z and Ctrl/Cmd+Y, Then Undo and Redo operate as expected within the current session
Multi-Language Policy Content Management
Given a template exists, When I add a translation for locale 'es', Then the UI requires Content for 'es' before allowing save Given multiple locales configured, When I preview in a chosen locale, Then the preview shows the content for that locale and renders tokens using that locale's formatting rules Given a template has no content for the storefront's locale, When the policy is displayed at checkout, Then the default language content is shown as a fallback Given I attempt to add a duplicate locale, When I save, Then validation blocks the duplicate and prompts me to edit the existing locale content
Organization-Wide Sharing and Attachment Availability
Given a template is marked Shared, When another member of the same organization opens the Attach Policy dialog for a class, Then the template appears in the list within 60 seconds Given a template is unshared, When members open the Attach Policy dialog, Then it no longer appears for new attachments but remains attached to existing classes and programs Given a shared template is updated and saved, When a member previews it from a class attachment, Then the updated content appears within 5 minutes
Checkout Disclosure and Enforcement Logic Integration
Given a class has a Cancellation policy and a Waiver template attached and in effect, When a customer reaches checkout, Then the disclosures display before payment with a required acknowledgment checkbox and the Pay button stays disabled until acknowledged Given a booking occurs at 10:00 with a 24-hour cancellation window, When the attendee attempts to cancel at 12:30 on the same day, Then the system blocks a full refund and applies the configured cancellation rule Given a No‑Show policy with a $15 fee is attached and effective, When the instructor marks an attendee as no‑show, Then the system charges $15 to the stored payment method or generates a payable invoice within 15 minutes and logs the action Given a Waiver is required, When the attendee does not accept/sign the waiver, Then the booking cannot be completed and no payment is captured Given policies have effective dates, When a policy changes after a booking is completed, Then the policy that was in effect at booking time governs that booking
Policy Import & Standardization
"As a studio owner, I want to import my existing policies and have them standardized so that I can start using them in ClassTap without recreating everything from scratch."
Description

Allow users to import existing policies from PDF, DOCX, or public URLs and automatically extract, classify, and map content into structured fields for ClassTap templates. Provide AI-assisted parsing with confidence scoring, highlight gaps and conflicts, and enable side-by-side manual review and edits before publishing. Preserve the original file for reference, support clause-level tagging, and ensure output conforms to platform schema for seamless use in enforcement and communications.

Acceptance Criteria
PDF Import and Field Mapping
Given a user selects a valid policy PDF and initiates import When the AI parser completes extraction Then the system maps detected content into template fields: title, cancellation_window, no_show_rules, refund_policy, waiver_text, terms, effective_dates, jurisdiction, exceptions And each mapped field includes a confidence score between 0 and 1 And unmapped or ambiguous content is listed as Unassigned with reasons And the result is saved as a draft template linked to the source file
DOCX Import with Clause-Level Tagging
Given a user uploads a policy in DOCX format When the system parses the document Then the content is segmented into clauses with unique IDs and clause types (Cancellation, No-Show, Waiver, Terms, Refund, Other) And each clause stores start/end offsets and tag(s) And mapped template fields reference one or more clause IDs And the user can add, edit, or remove clause tags prior to publish And edits to clause tags update field mappings accordingly
Public URL Import and Source Preservation
Given a user provides a publicly accessible URL to a policy When the system fetches the content Then it creates an immutable snapshot (PDF or HTML archive) stored with the import record And metadata captured includes source_url, fetch_timestamp, and content_type And the original snapshot is downloadable from the resulting template And extraction and mapping proceed as with file uploads
Confidence Scoring and Low-Confidence Handling
Given each extracted field includes a confidence score When any required field has a score below the platform threshold (default 0.8) Then the field is flagged Needs review and Publish is disabled And viewing the field reveals the linked source clause and reason indicators And after the user confirms or corrects the value, the flag clears and the field is considered resolved
Gap and Conflict Detection
Given the platform schema defines required fields and business rules When extraction completes Then missing required fields are listed as Gaps with suggested source snippets And contradictory values across clauses are listed as Conflicts with explanations And the user can resolve each gap or conflict via field edits or clause tag updates And Publish remains disabled until all blockers are resolved
Side-by-Side Review and Edit
Given a draft template exists from an import When the user opens the review screen Then the left pane shows structured fields and the right pane shows the source with clause highlights And selecting a field scrolls to and highlights the referenced clause(s) And edits to structured fields update mappings and create a changelog entry with timestamp and actor And the user can accept all changes and publish the template
Schema Validation and Template Readiness
Given the user attempts to publish a reviewed template When server-side schema validation runs Then the template conforms to the platform schema and passes validation And on success the template status changes to Ready and can be auto-attached to selected classes/programs And on failure the publish is blocked with specific field-level error messages and codes And the API returns HTTP 422 with a machine-readable error payload for invalid submissions
Auto-Attach Rules Engine
"As an administrator, I want policies to auto-apply to the right classes based on rules so that I don’t have to manually attach them every time."
Description

Create a rule-based system to automatically attach policy templates to classes and programs based on attributes such as class type, location, instructor, tags, price range, and schedule. Support precedence, inheritance, and default fallbacks, with conflict detection and simulation to preview outcomes. Include bulk apply and backfill, plus audit logging of which rules applied and when, ensuring minimal manual effort and consistent policy coverage.

Acceptance Criteria
Auto-attach policies by class/program attributes
Given a new class with attributes type=Yoga, location=Studio A, instructor=ID123, tags=[Beginner, Heated], price=25, schedule=2025-09-10T18:00Z and a rule R1 that matches all those attributes and maps to policy template P1 When the class is saved Then template P1 is attached to the class within 1 second And the attached template is visible in the class Policies section And no additional policy templates are attached And the same behavior applies when creating a program with matching attributes
Rule precedence and deterministic tie-breaking
Given two matching rules R1 and R2 for a class, where R1.priority=90 and R2.priority=80 When the class is saved Then the template from R1 is attached and the template from R2 is not And the audit records that R1 won due to higher priority Given two matching rules R3 and R4 with equal priority When both match the same class Then the rule with greater specificity (more attribute conditions) wins; if specificity is equal, the most recently updated rule wins And the chosen basis (specificity or last-updated) is recorded in the audit
Inheritance and default fallback application
Given a program has policy template P_prog attached via rule PR And a class within that program has no matching class-level rule When the class is saved Then the class inherits P_prog without duplicating program-level attachments Given no rules match at program or class level and a default template D is configured When the class is saved Then template D is attached And if no default template is configured, no template is attached and a warning is surfaced in the UI and logs
Conflict detection and resolution gating
Given two matching rules would attach templates P_A and P_B that define conflicting policy categories (e.g., multiple cancellation windows) When the user saves the class Then the system blocks the save and displays a conflict summary listing the rules, templates, and policy keys in conflict And the user is offered resolution options: choose one template, adjust priorities, or cancel And no policy attachment is persisted until the conflict is resolved And conflict details are captured in the audit log
Simulation/preview of rule outcomes
Given a user opens the Simulator for a draft class with specified attributes When the user clicks Simulate Then the system displays predicted template(s), matching rule IDs, priorities, and whether inheritance or default fallback applies And any conflicts are flagged with the same detail as on save And no changes are persisted by simulation And response time is under 1.5 seconds for a single class and under 10 seconds for a batch of 500 entities
Bulk apply and backfill with dry-run
Given an admin selects Bulk Apply with filters (date range, locations, instructors, tags) and enables Dry Run When the job executes Then the system reports counts: evaluated, updated, unchanged, conflicts, and failures, with sample IDs for each bucket And no entities are modified during Dry Run When the admin runs the same job with Dry Run disabled Then policies are attached/updated per current rules And entities with conflicts are skipped and listed in the report And the job processes at least 2,000 entities within 5 minutes and is resumable if interrupted
Audit logging and traceability
Given a policy template is attached, updated, or removed via the rules engine When viewing the audit log for a class or program Then each entry includes timestamp, entity ID, actor (system/user), operation (attach/update/remove), rule ID and version, template ID and version, and a summary of matched conditions And audit entries are immutable and exportable to CSV for a given date range And the API endpoint GET /audit?entityId={id}&from={t1}&to={t2} returns the same data with pagination and filtering by operation type
Consent Capture & E‑Sign at Booking
"As a customer, I want to review and sign required waivers during booking so that I can complete checkout knowing what I’m agreeing to."
Description

Integrate policy presentation and acceptance into checkout and account onboarding. Display the correct waiver/terms version, capture explicit consent via checkbox and electronic signature, and support per-attendee consent including guardian consent for minors. Record immutable evidence (policy version, timestamp, IP/device, user ID), generate a downloadable confirmation, and prompt for re-consent when a policy version changes. Ensure a mobile-first, WCAG-compliant experience.

Acceptance Criteria
Correct Policy Version Display at Booking
Given a class/program has a policy template mapped in Policy Port with version V effective on date D When a user begins checkout for a session occurring on/after D Then the checkout displays the mapped policy content with template name, version V, and effective date D Given no class/program-specific mapping exists When a user begins checkout Then the system displays the account-level default policy template(s) and current version(s) from Policy Port Given both class/program-level and account-level templates exist When a user begins checkout Then the class/program-level mapping takes precedence over account defaults
Explicit Checkbox and E‑Signature Required to Complete Booking
Given the policy content is displayed When the required "I agree" checkbox is not checked Then the "Complete Booking" action is disabled and an accessible inline error is shown if attempted Given the checkbox is checked but no electronic signature (drawn or typed) is provided When the user attempts to complete booking Then submission is blocked and an accessible error indicates signature is required Given a valid signature is provided and the checkbox is checked When the user completes booking Then the system captures and stores the signature artifact (image or typed rendering) and its cryptographic hash linked to the consent record
Per‑Attendee and Guardian Consent Handling
Given multiple attendees are included in a single booking When proceeding through consent Then consent is required per attendee and the booking cannot complete until all attendees have satisfied consent requirements Given an attendee has an existing consent on file for the same template and current version When included in a new booking Then the attendee is marked as "Consent on file" and re-signing is not required Given an attendee’s date of birth indicates they are under the configured age of consent (default 18) When collecting consent Then guardian full name, relationship, contact info, and guardian e‑signature are required, and the minor cannot self-consent Given multiple minors are in the booking When collecting guardian consent Then configuration determines whether one guardian may consent for multiple minors; the UI enforces the configured rule
Immutable Evidence and Audit Trail
Given a booking is successfully completed with consent When writing the consent record Then an append-only audit record is stored containing: booking ID, attendee ID(s), booker user ID (if any), guardian ID (if any), policy template ID, policy version, policy content hash (SHA-256), consent timestamp (UTC ISO 8601), IP address, user agent, device fingerprint (if available), signature hash, and locale Given the evidence record exists When an update or delete is attempted via UI or API Then the operation is blocked (HTTP 403/UI error) and the attempt is logged; only append operations are allowed Given the evidence write fails for any reason When the user attempts to complete booking Then the booking is aborted, no payment is captured, and the user sees a non-destructive error prompting retry
Downloadable Consent Confirmation
Given a booking completes with consent When generating confirmation artifacts Then a downloadable PDF is created within 60 seconds containing: booking ID, class/program name, attendee name(s), policy template name, version, policy content hash, consent timestamp, signer name(s), IP (abbreviated as required by privacy settings), and signature image(s) Given the PDF is generated When presenting confirmation to the user Then a download link appears on the confirmation screen and is included in the confirmation email; the link remains accessible for at least 24 months or until the account is deleted Given the PDF is downloaded When its hash is verified Then it matches the stored evidence hash for the consent record
Re‑Consent Prompt on Policy Version Change
Given a user has previously consented to version X of a policy When version Y (>X) becomes effective for the class/program or account Then at next checkout or account onboarding the user is prompted to review and must re-consent to version Y before completion Given a user has already consented to the current version When booking another class Then the flow indicates "Consent on file" and does not require re-consent Given the policy version changes during an in-progress checkout When the user attempts to submit Then the consent step refreshes to the new version and requires fresh consent before proceeding
Mobile‑First and WCAG 2.2 AA Compliance
Given a mobile viewport between 320–414 px width in portrait When viewing the consent step Then all controls (policy content, checkbox, signature pad, buttons) are usable without horizontal scrolling and meet touch target size >= 44x44 px Given a keyboard-only or screen reader user When navigating the consent step Then focus order is logical, labels/aria are present, checkbox state and submit state are announced, and errors are programmatically associated and announced Given the user zooms to 200% When viewing the consent step Then content reflows without loss of functionality and maintains contrast >= 4.5:1 for text and controls Given a user cannot draw a signature When providing consent Then a type-to-sign alternative is available and produces an equivalent signature artifact linked to the consent record
Cancellation & No‑Show Enforcement Engine
"As an instructor, I want the system to automatically enforce my cancellation and no‑show rules so that I reduce disputes and administrative workload."
Description

Apply policy logic at booking, modification, and attendance events to enforce cancellation windows and no‑show rules. Calculate eligibility for refunds, fees, or credits; automate charges and refunds; prevent late cancellations when disallowed; and align with waitlist-to-payment flows. Handle edge cases like multi-session programs, drop-in vs. series bookings, time zones, and instructor overrides with audit notes, ensuring predictable outcomes and reduced disputes.

Acceptance Criteria
Attach Policy at Booking and Compute Deadlines
Given a drop-in class priced $40 scheduled for 2025-10-12T09:00:00-07:00 in America/Los_Angeles and policy P1 (free cancellation until 24h before start; late cancellation disallowed; no-show forfeits 100%) When Student S books at 2025-10-10T09:00:00-07:00 Then the booking stores policy_snapshot.id = P1, cancel_deadline_local = 2025-10-11T09:00:00-07:00, and cancel_deadline_utc = 2025-10-11T16:00:00Z And the booking detail UI displays “Free cancel until Oct 11, 9:00 AM PDT” And an audit log entry is recorded with action = policy_attached, actor = system, booking_id, policy_version, and timestamps
Late Cancellation Enforcement and Fee Handling
Given a class priced $40 scheduled for 2025-10-12T09:00:00-07:00 with policy P2 (cancel allowed until 2h before; late cancellation disallowed) When the student attempts to cancel at 2025-10-12T08:00:00-07:00 (1h before) Then the API rejects the request with HTTP 409 and error_code = "LATE_CANCELLATION_DISALLOWED" And the Cancel button is disabled in the UI with helper text “Cancellation window has passed” Given a class priced $40 scheduled for 2025-10-12T09:00:00-07:00 with policy P3 (cancel allowed until 12h before; within 12h late cancellation allowed with a 50% fee) When the student cancels at 2025-10-12T04:00:00-07:00 (5h before) Then the system confirms the late cancellation, charges a $20 fee (50%), and issues a $20 refund/credit as configured And receipts are emailed for both fee and refund, and ledger entries reflect net amount = $0
Automated Refunds, Charges, and Idempotency
Given a paid booking of $40 captured at booking and policy P1 (free cancellation until 24h) When the student cancels at T = start_time - 30h Then a full refund of $40 is automatically initiated with gateway_reference captured, and booking status becomes CANCELED_REFUNDED And a receipt email is sent to the student and an audit log entry is created with action = refund_initiated And repeating the same cancel request within 24h with the same idempotency_key returns HTTP 200 and the original refund transaction_id without creating a duplicate transaction
Waitlist Promotion Alignment with Cancellation Policy
Given a class priced $30 with policy P4 (cancel allowed until 12h before; inside window no refunds) and waitlist auto-promotion set to auto-charge upon seat open And a seat opens at 2025-10-12T06:00:00-07:00 for a class starting at 2025-10-12T09:00:00-07:00 When Student S is auto-promoted and charged at 2025-10-12T06:00:05-07:00 (inside late window) Then the booking is marked as inside_late_window = true and the UI label reads “Late cancellation: no refund” And if Student S cancels at 2025-10-12T06:30:00-07:00, no refund is issued and the ledger records revenue retained per policy And the system does not issue any duplicate charges during promotion or cancellation, verified by a single captured transaction_id
Multi-Session Program: Program-Level vs Per-Session Policies
Given an 8-session program priced $160 total ($20 per session) And policy P5: program-level cancellation allowed ≥48h before first session for full refund; after first session start, per-session cancellations allowed ≥24h before the session for a session credit When the student cancels the entire program 72h before the first session Then a full $160 refund is issued and all sessions are released When the student cancels only Session 3 at T = session3_start - 26h Then a $20 credit is issued and only Session 3 is released When the student attempts to cancel Session 4 at T = session4_start - 12h Then the API rejects with HTTP 409 and error_code = "LATE_SESSION_CANCELLATION_DISALLOWED" and no credit is issued
Time Zone and DST-Accurate Enforcement
Given a class located in America/Los_Angeles starting 2025-11-02T09:00:00-08:00 (PST after DST end) with policy P1 (free cancellation until 24h) When the system computes the cancellation deadline Then cancel_deadline_local = 2025-11-01T09:00:00-07:00 (PDT) and cancel_deadline_utc = 2025-11-01T16:00:00Z And a student in America/New_York sees the deadline as 2025-11-01T12:00:00-04:00 and the class start as 2025-11-02T12:00:00-05:00 And cancellation attempts are validated against the class location time zone regardless of the student’s device time zone
Instructor/Admin Override with Mandatory Audit Notes
Given a late cancellation disallowed by policy P2 resulting in no refund by default When an instructor with override permission waives the fee and issues a full refund Then the system requires an override_reason with at least 10 characters and records an audit log with before/after financials, actor_id, timestamp, and policy_snapshot And the refund is executed once, receipts are sent, and the booking shows override_applied = true with a link to the audit record And if a user without override permission attempts the same action, the API returns HTTP 403 and no financial changes occur
Versioning, Effective Dating & Audit Trail
"As a studio owner, I want versioned policies with an audit trail so that I can prove what terms applied to a booking in case of a dispute."
Description

Provide full version control for policy templates with effective start/end dates and immutable snapshots applied to each booking. Offer diff views, author and timestamp metadata, and searchable history to identify which version a participant accepted. Enable rollback to prior versions, exportable audit logs for compliance reviews, and alerts when changing an active policy that impacts upcoming bookings.

Acceptance Criteria
Create Policy Version with Effective Dates and Validation
Given a policy template exists with at least one version When a user creates a new version and sets an effective start date-time (and optional end date-time) Then the system rejects the version if its effective window overlaps any existing version for the same template And rejects if start date-time is missing or is not earlier than end date-time when end is provided And accepts an open-ended end date-time (no end) as valid And stores effective timestamps in UTC while displaying them in the organization’s timezone And assigns the version state as Active when start <= now and (no end or end > now), Scheduled when start > now, and Expired when end <= now And surfaces clear validation messages for any failures
Immutable Policy Snapshot Applied at Booking Checkout
Given a class/program has a policy template attached and a version is currently Active When a participant completes checkout and accepts the policy Then the booking is stamped with the policy template ID, version ID, acceptance timestamp, and accepting user identifier And an immutable snapshot of the policy text and structured fields is stored with a content hash And subsequent edits or new versions do not alter the stored snapshot for that booking And booking detail (UI and API) exposes the version ID, acceptance timestamp, and link to the snapshot
Version Diff View for Policy Templates
Given at least two versions of the same policy template exist When a user opens the compare view between Version X and Version Y Then a side-by-side and inline diff highlights added, removed, and modified content And structured fields (e.g., cancellation window hours, fees, terms toggles) show old vs new values And metadata (author, created timestamp, change summary) for each version is displayed And whitespace-only changes can be toggled to be ignored in the diff
Version Metadata and Searchable History
Given multiple versions and bookings reference a policy template When a user searches history by policy name/template ID, version ID, author, date range, or booking/participant identifier Then the results include versions matching the filters with author and timestamp metadata And for booking/participant queries, the result identifies the exact policy version accepted and links to the booking and snapshot And results are sortable by effective start, created timestamp, and author
Rollback to Prior Policy Version
Given a policy template with historical versions When a user initiates a rollback from the current version to a selected prior version Then the system creates a new version that copies the selected prior version’s content and notes the rollback source in the change summary And requires setting a non-overlapping effective start date-time prior to saving And preserves all version history without deletion or mutation of prior records And records author and timestamp metadata for the rollback action
Exportable Policy Audit Logs
Given versioning activity has occurred for one or more policy templates When a user exports the audit log for selected templates and a date range Then a downloadable CSV and JSON are generated containing events (create, update, activate, deactivate, rollback, booking acceptance) And each record includes at minimum: event type, template ID, version ID (if applicable), actor, UTC timestamp, description, and affected bookings count (if applicable) And files are UTF-8 encoded with headers and are retrievable via UI and authenticated API
Alerts on Changes to Active Policies with Upcoming Bookings
Given an active policy version exists and there are upcoming bookings in the future When a user edits the active version or changes its effective window in a way that would impact future bookings Then the system displays a blocking alert showing the number of upcoming bookings and affected classes/programs And requires explicit confirmation to proceed, with an option to schedule changes via a new future-dated version instead And clearly states that existing bookings retain their original policy snapshots and are not retroactively altered
Policy Communication in Customer Touchpoints
"As a customer, I want clear policy information wherever I view or book a class so that I understand expectations before I commit."
Description

Surface policy summaries and links consistently across class pages, checkout, confirmations, reminders, and receipts. Inject dynamic snippets (e.g., “Cancel up to 12 hours before start”) into email/SMS templates with correct time zone handling. Display policy badges on listings, deep-link to full policy content, and support A/B testing of summary phrasing to improve clarity and reduce no-shows and cancellations.

Acceptance Criteria
Policy Summary and Badge on Class and Listing Pages
Given a published class with an attached policy template containing a summary (<=180 characters) and badge When a visitor opens the class details page on desktop or mobile Then the policy badge is visible without scrolling, and the summary text matches the template exactly, and a "View full policy" deep link is present and resolves with HTTP 200 Given the same class appears in a program or search listing card When the listing renders Then the policy badge icon displays, and hovering/tapping shows the summary tooltip within 300ms Given a class without an attached policy When the page renders Then no policy badge, summary, or link is shown Given the policy template summary is updated and published When the class page is refreshed Then the summary and badge reflect the update within 5 minutes
Checkout Policy Summary and Required Acknowledgment
Given a class with an attached policy template When a customer reaches the checkout review step Then the policy summary is displayed with a checkbox labeled "I agree to the policy", pre-unchecked, and the Pay/Complete button is disabled until checked Given the customer checks the acknowledgment checkbox When they complete payment Then the order stores policy_template_id, policy_version_id, and acceptance_timestamp, and the receipt references the same version Given the checkbox is not checked When the customer attempts submission via click, tap, or Enter key Then submission is blocked and an inline error "Please acknowledge the policy to continue" appears and is announced by screen readers Given A/B variants exist for the policy summary When checkout renders Then the assigned variant text is displayed consistently for that user/session for the entire checkout flow
Transactional Emails and SMS Carry Policy Snippets
Given a successful booking for a class with an attached policy When sending booking confirmation email and SMS Then each message includes the policy summary snippet and a deep link to the full policy, and messages send successfully with 2xx response from the provider Given a reminder is scheduled 24 hours before class start When the reminder is dispatched Then the same policy snippet and link are included, matching the policy version captured at booking Given SMS length constraints When the assembled SMS exceeds 160 GSM-7 characters Then the policy snippet is truncated to keep the total <=160 characters without splitting words, and the link remains intact Given the class has no attached policy at booking time When messages send Then no policy snippet or badge text appears
Deep Link Resolves to Versioned Full Policy Content
Given a booking captured under policy version V When the recipient clicks the "View full policy" link in any touchpoint Then the link resolves to the exact content of version V, immutable after later edits, with HTTP 200 and TTFB <= 1 second Given the merchant updates the policy template to version V+1 after the booking When the old link is clicked Then it still resolves to version V, and the page clearly shows "Effective on [date/time] — Version V" Given the deep link is shared publicly When accessed without authentication Then the content renders read-only with no PII and includes merchant branding
A/B Testing of Policy Summary Phrasing
Given two active variants (A and B) for a policy summary When visitors are first exposed on any touchpoint for a given class Then they are randomly assigned a variant with 50/50 split ±2% and persisted for 30 days via cookie/local storage and user ID Given a user assigned to a variant When they see the summary across class page, checkout, and transactional messages for the same booking Then the same variant phrasing is used consistently Given experimental analytics are enabled When summaries are rendered or links clicked Then impression and click events are logged with variant, class_id, user_id (if available), timestamp, and touchpoint, and downstream outcomes (cancellation, no-show) are attributed to the variant Given an experiment is toggled off When pages render thereafter Then the control phrasing is used and no new assignment events are logged
Time Zone Correctness in Policy Snippets
Given a class scheduled 2025-10-15 10:00 America/Los_Angeles with a 12-hour cancellation window and a recipient in America/New_York When generating the policy snippet with an absolute cutoff Then the cutoff is computed as 2025-10-14 22:00 America/Los_Angeles and displayed to the recipient as 2025-10-15 01:00 EDT, including the EDT label Given the recipient’s time zone is unknown When generating the snippet Then the cutoff time is displayed in the class’s time zone with its abbreviation and GMT offset Given a class spans a daylight saving transition When computing the cutoff Then the calculation uses IANA time zone rules for the class’s zone and produces the correct local time Given the snippet uses relative phrasing only (e.g., "Cancel up to 12 hours before start") When rendered Then no absolute times are shown and no time zone label is appended

Test‑Drive Checkout

Run a full sandbox of the booking flow—reservation, waitlist auto‑fill, and payment with test cards—before you publish. Share a preview link with teammates to spot friction and fix it fast, ensuring a smooth first impression for clients.

Requirements

Sandbox Mode Toggle
"As a studio owner, I want to switch a class to a safe sandbox that mirrors live behavior so that I can test the full checkout flow without risking real charges or data pollution."
Description

Provide a fully isolated sandbox environment that mirrors production configurations (classes, pricing, taxes, capacities, policies, resources, branding, and time zones) and is clearly labeled as Test. When enabled, all bookings, payments, and data writes occur in a non-production partition; external side effects (live charges, calendar sync, marketing automations, and analytics) are suppressed or routed to safe stubs. Admins can enable sandbox per class, schedule, or account-wide prior to publishing. Visual indicators appear across admin and client views to prevent confusion. The toggle must be reversible without data leakage between environments and must preserve parity of validations, error handling, and latency characteristics to ensure realistic testing outcomes.

Acceptance Criteria
Sandbox Mirrors Production Configurations
Given an account has production configurations for classes, pricing, taxes, capacities, policies, resources, branding, and time zones When Sandbox Mode is enabled for that scope Then the sandbox environment contains an identical copy of those configurations within 60 seconds And all computed business outcomes (capacity checks, tax totals, discount applications) match production for identical inputs And any parity discrepancy is logged with severity High, including field name, scope, and timestamp
Isolated Non‑Production Data Partition
Given Sandbox Mode is enabled for a scope (account, schedule, or class) When a booking, waitlist entry, refund, or payment is created in sandbox Then the record is persisted only in the sandbox data partition with an environment identifier And querying production APIs/UIs for that record returns no result (404 or empty set) And production exports and dashboards exclude sandbox records And sandbox identifiers are globally unique and cannot collide with production identifiers
External Integrations Stubbed in Sandbox
Given Sandbox Mode is enabled When a payment is attempted Then only test gateways and approved test card numbers are accepted and no live charge is created Given Sandbox Mode is enabled When calendar sync runs Then events are written only to a designated test calendar or suppressed while returning a stubbed success Given Sandbox Mode is enabled When marketing automations would trigger Then no live send occurs and events are routed to a test list or stubbed sink Given Sandbox Mode is enabled When analytics events fire Then they are sent only to test properties and include an environment=sandbox flag And all outbound webhooks include header X-Environment: sandbox and target only test endpoints
Granular Enablement: Account, Schedule, and Class
Given an admin with permissions When they set Sandbox Mode at the account level Then all unpublished schedules and classes inherit sandbox status by default Given Sandbox Mode is set at multiple levels When conflicts occur Then precedence is Class > Schedule > Account and the effective scope is displayed in the UI Given a class is Published When an admin attempts to enable Sandbox Mode for that class Then the action is blocked with guidance to unpublish or use a preview link Given Sandbox Mode is enabled for a class or schedule When a preview link is generated Then the link routes users only to sandbox flows and is clearly marked as a test preview
Visual Test Indicators in Admin and Client Views
Given Sandbox Mode is enabled When any admin or client-facing page in the booking and checkout flow is loaded Then a persistent Test badge and contrasting banner are visible above the fold on every page And the page title contains [TEST] and payment forms display "Test Mode — use test cards only" And all transactional emails, receipts, and SMS include [TEST] in subject/body And indicators meet WCAG 2.1 AA contrast and include aria-labels for screen readers And theming/branding cannot hide or alter the Test indicators
Toggle Reversibility Without Data Leakage
Given Sandbox Mode is enabled for a scope with existing sandbox data When Sandbox Mode is disabled Then new actions write only to production and historical sandbox data remains isolated and queryable in sandbox And no sandbox records appear in production UIs, APIs, or reports And re-enabling Sandbox Mode restores access to prior sandbox data without migration And environment switching completes within 10 seconds and is logged with user, timestamp, scope, and result
Parity of Validations, Errors, Latency, and Waitlist Auto‑Fill
Given identical inputs applied to production and sandbox (using test gateways where applicable) When validation rules execute Then the same rules, error codes, and message texts are returned for both environments Given identical booking and payment flows When measuring end-to-end latency over 200 transactions Then median latency differs by no more than 10% and P95 by no more than 15% between environments Given a class reaches capacity in sandbox When a seat becomes available Then waitlist auto-fill triggers using the same priority rules and timings as production and requires successful test payment to confirm the spot
Shareable Preview Links and Access Control
"As a teammate helping review checkout, I want a secure preview link I can open on my device so that I can spot friction and report issues without needing admin credentials."
Description

Allow admins to generate expiring, revocable preview URLs for the sandbox checkout that render the client-facing experience on desktop and mobile. Links can be scoped (single-use or multi-use), protected (password, email-domain allowlist), and time-bound, with full access logs (creator, viewers, timestamps, IP). Admins can invalidate links at any time. Preview users experience the end-to-end flow, including forms, policies, and upsells, but all side effects are sandboxed. Integration respects existing team roles and SSO where applicable, enabling collaboration without granting full admin access.

Acceptance Criteria
Time-Bound Expiring Preview Link Generation
Given I am an admin with permission to create preview links When I set an absolute UTC expiration datetime within 5 minutes to 30 days and generate the link Then the link is created with a unique, unguessable token and stored metadata including creator_id and expiry And accessing the link before expiry loads the sandbox checkout successfully (HTTP 200) And accessing the link after expiry returns HTTP 410 with error code PREVIEW_LINK_EXPIRED And attempts to set expiry outside allowed bounds are rejected with HTTP 422 and validation messages
Revocation of Active Preview Links
Given an active preview link exists When an admin selects Invalidate Now and confirms Then the link becomes unusable globally within 5 seconds And any new requests receive HTTP 410 with error code PREVIEW_LINK_REVOKED And any in-progress preview sessions are shown a non-dismissible notice and further actions are blocked And a revocation event is appended to the audit log with admin id, timestamp (UTC), and optional reason
Scoped Access: Single-Use vs Multi-Use
Given a preview link configured as single-use When the first session successfully loads the preview Then usage_count increments to 1 and subsequent requests return HTTP 403 with error PREVIEW_LINK_USED Given a preview link configured as multi-use with a cap N When N distinct sessions load the preview Then the (N+1)th request returns HTTP 429 with error PREVIEW_LINK_LIMIT_REACHED And repeated visits from the same session (token + browser session id) do not increment usage_count And current usage and cap are visible to admins in the link details
Protected Access: Password and Email-Domain Allowlist
Given password protection is enabled When a viewer opens the link Then they must enter the correct password over TLS before content loads And after 5 failed attempts within 15 minutes, the originating IP is rate-limited for 15 minutes (HTTP 429 PREVIEW_AUTH_RATE_LIMIT) And the password is stored salted and hashed (Argon2id) and never returned via API/UI Given email-domain allowlist is enabled with @company.com When a viewer verifies email via magic link or SSO Then only addresses ending with @company.com are granted access; others receive HTTP 403 PREVIEW_DOMAIN_BLOCKED And when both protections are enabled, both must pass for access
Access Logging: Creator, Viewers, Timestamps, IP
Given preview links are accessed or attempted When any request (success or denied) occurs Then an access log entry is recorded with link_id, creator_id, event_type (view_granted, view_denied, revoked, expired), timestamp (UTC ISO 8601), source_ip, user_agent, outcome, and viewer_identifier (email/SSO subject or anonymous) And logs are immutable, filterable by date range and link_id, and exportable to CSV And exported logs anonymize IPs (IPv4 /24, IPv6 /48) and redact PII per policy And listing the last 10,000 events for a link returns within 2 seconds in 95th percentile
Sandboxed End-to-End Flow Parity
Given a viewer accesses the preview link on desktop or mobile When the sandbox checkout loads Then it renders the full client-facing flow including forms, policies, upsells, waitlist auto-fill, and payment using test cards And all side effects are isolated: no real charges, invoices, SMS/email, or inventory changes; external integrations run in test mode only And test card numbers are accepted and clearly labeled as Sandbox; real cards are rejected with explanatory messaging And admins can reset sandbox state per link and observe that data is cleared without affecting production
Role and SSO Respect in Preview Access
Given the organization uses team roles and SSO When a signed-in teammate with permitted role accesses the preview Then their existing SSO session is honored for identity, but they receive preview-only viewer access and no admin console privileges And attempts by users without create/revoke/log permissions result in HTTP 403 PREVIEW_PERMISSION_DENIED for those actions And the audit trail records creator, sharer, changes to protections, and revocations with timestamps and actor IDs
Simulated Payments with Test Cards and 3DS Flows
"As an instructor configuring payments, I want to run checkout with test cards and force different outcomes so that I can verify success, 3DS challenges, and error handling before going live."
Description

Enable test-mode payment processing that supports gateway-specific test cards, success and failure scenarios (insufficient funds, card declined, expired card, network error), and 3D Secure challenge simulations. Webhook events are mocked or routed to a sandbox endpoint, preserving event order and timing to validate downstream automations (confirmations, invoices, promo codes, taxes, stored methods, refunds, partial payments, and credits). Provide a selector to force specific outcomes for reproducible tests. No live tokens or charges are created, but full receipts and ledger entries are generated in test partition for verification.

Acceptance Criteria
Test Mode Toggle and Outcome Selector
- Given an admin is configuring Test‑Drive Checkout, When Test Mode is enabled, Then all payment requests are routed to sandbox services and no live tokens or charges are created. - Given Test Mode is enabled, When the Outcome Selector is opened, Then the tester can choose Success, Insufficient Funds, Card Declined, Expired Card, Network Error, 3DS Pass, 3DS Fail, or 3DS Timeout. - Given a specific outcome is selected, When a payment is attempted, Then the transaction deterministically returns the selected outcome and records the forced outcome identifier in the audit log. - Given no outcome is selected, When a payment is attempted with a supported test card, Then the outcome is determined solely by the test card number rules without randomization. - Given Test Mode is enabled, Then all checkout and admin surfaces display a visible "Test Mode" badge and the environment is clearly labeled on receipts and logs.
Gateway Test Cards: Success and Failure Matrix
- Given documented gateway test cards for approval and error cases, When each respective card is used, Then the system returns the correct status and machine code per case: approved, insufficient_funds, card_declined, expired_card, processing_error(network). - Given any error case occurs, Then the UI displays a human‑readable message mapped to the machine code and the API returns a consistent error schema with code and reason. - Given any supported test card is used, Then tokenization uses mock tokens and primary account numbers are never transmitted to live endpoints. - Given a network error case is forced or triggered, When retries occur, Then exponential backoff is applied up to 3 attempts and the final state is failed with retry details captured in logs. - Given a successful test authorization and capture, Then the payment record shows status = succeeded and includes gateway test identifiers for traceability.
3D Secure (3DS) Challenge Simulation
- Given a test card that requires 3DS, When payment is submitted, Then a simulated 3DS flow is presented in an embedded challenge window within the checkout. - Given the Outcome Selector is set to 3DS Pass, When the challenge is completed, Then the payment transitions to succeeded with liability_shift = true and a 3DS authentication record stored in the test partition. - Given the Outcome Selector is set to 3DS Fail or 3DS Timeout, When the challenge ends or 60s elapses, Then the payment transitions to failed with code = requires_payment_method and no charge is created. - Given a frictionless 3DS test card is used, When payment is submitted, Then no challenge UI appears and the payment proceeds directly to succeeded with appropriate 3DS indicators. - Given the tester cancels the 3DS challenge, Then the payment is marked failed with a cancel reason and the cart remains unpaid.
Sandbox Webhooks: Ordering, Timing, and Delivery
- Given a test payment lifecycle occurs, When webhook events are emitted, Then they are delivered only to the configured sandbox endpoint and never to production endpoints. - Then event order is preserved per contract (e.g., payment_intent.created → payment_method.attached → payment_intent.succeeded → charge.captured) and inter‑event delays are simulated within 100–1500 ms. - Given the receiver returns 5xx, When delivery fails, Then retries use exponential backoff up to 5 attempts with idempotency keys reused on retries and each attempt logged in the sandbox event log. - Given webhook signing is enabled, When events are delivered, Then signatures validate with the sandbox secret and invalid signatures are rejected and retried. - Given an event is undeliverable after max retries, Then the system flags it for manual replay and exposes a "Replay" control in the sandbox event log.
Refunds, Partial Payments, and Credits in Test Mode
- Given a succeeded test payment exists, When a full refund is initiated, Then a refund object is created in the test partition, corresponding webhook events are emitted in order, the ledger balance is updated, and a "Test Refund" receipt is generated. - Given a succeeded test payment exists, When a partial refund of a specified amount or percentage is initiated, Then taxes and promo discounts are prorated consistently and the customer's credits balance is incremented accordingly. - Given an order with an outstanding balance exists in test mode, When a partial payment is made, Then a new test transaction linked to the order is created and the remaining balance is updated accurately. - Given credits are applied during a test checkout, When the transaction completes, Then the ledger logs credit consumption, receipts show credits applied, and the webhook payload includes credits_applied metadata. - Given any refund in test mode, Then no live processor refund is attempted and all artifacts remain scoped to the test partition.
Receipts, Invoices, and Ledger Partitioning
- Given any test transaction (payment or refund), Then all artifacts (receipts, invoices, ledger entries) are stored under a TEST partition and labeled "Test Mode — No live charge". - Given an admin exports financial reports, When the default filters are applied, Then test artifacts are excluded; When "Include Test Data" is toggled, Then test artifacts are included in the export with a partition flag. - Given notification templates are configured, When a test transaction completes, Then only preview/sandbox notifications are sent to preview recipients and no client‑facing live emails/SMS are sent. - Given a receipt is generated in test mode, Then it displays the test card brand and masked last4 consistent with the test card used and includes the forced outcome (if any) in metadata. - Given an auditor views the test ledger, Then every entry links to its webhook event trail and processor test identifiers for end‑to‑end traceability.
Waitlist Auto‑Fill to Test Payment Flow
- Given a class is full and a user is on the waitlist in sandbox, When a seat opens, Then the system reserves the seat for 10 minutes and sends a preview notification containing a payment link. - Given checkout is completed within the reserve window and the forced outcome is Success, Then the attendee status becomes Confirmed, the seat is retained, and all related webhooks fire in order. - Given the forced outcome is an error (e.g., insufficient funds), When payment is attempted, Then the reservation remains pending until payment succeeds or the 10‑minute window expires, after which the seat is released and the waitlist advances. - Given a network error outcome is forced, When retries are exhausted, Then the tester is prompted to retry manually and no seat is permanently held. - Given the reservation expires without successful payment, Then the system records the expiration event in the sandbox log and no live notifications are sent to clients.
Waitlist Auto-Fill Simulation
"As a studio admin, I want to simulate waitlist promotions and payment prompts so that I can ensure the auto-fill logic and communications behave as expected."
Description

Replicate the full waitlist lifecycle in sandbox: reaching capacity, collecting waitlist entries, triggering seat release, auto-promoting waitlisted customers, and requesting payment within configured time windows. Support edge cases such as simultaneous promotions, no-response expirations, cancellations, and class time changes. Provide controls to fast-forward triggers and inject events (e.g., simulate a cancellation) to validate fairness rules, notifications, and payment prompts. All actions create a sandbox audit trail to verify policy adherence and user experience.

Acceptance Criteria
Capacity Reached and Waitlist Intake
Given a class with capacity set to N in sandbox And N successful test reservations are completed When an additional customer attempts to book the class Then the system prevents a direct reservation and offers a waitlist option And upon waitlist opt‑in, the entry is recorded with timestamp, customer ID, and source And the waitlist count increases by 1 and is visible in the sandbox view And an audit trail entry is created capturing action, actor, time, and class ID
Auto‑Promotion and Payment Window Enforcement
Given a class with at least one waitlisted customer and a configured payment hold window (e.g., 15 minutes) And a seat becomes available in sandbox When auto‑promotion is triggered Then the next eligible waitlisted customer is promoted per configured fairness rule (e.g., FIFO) And a payment prompt using test cards is sent in sandbox and logged And the seat is held exclusively for the promoted customer for exactly the configured window And if payment succeeds within the window, the reservation is confirmed and the waitlist count decreases And if payment does not succeed by window expiry, the hold is released and the next eligible customer is auto‑promoted And all actions are captured in the sandbox audit trail with status transitions
Simultaneous Seat Releases and Race‑Condition Handling
Given two or more seats are released within the same second And there are multiple waitlisted customers When auto‑promotion runs concurrently Then promotions are processed atomically with deterministic ordering per fairness rules And no customer receives more than one active hold at a time And total confirmed + active holds never exceed class capacity at any point And in the event of a tie, the documented tiebreaker (e.g., earlier waitlist timestamp, then customer ID) is applied And the audit trail shows a single, ordered sequence with no interleaving that violates capacity or ordering
Injected Cancellation Triggers Promotion
Given a confirmed attendee exists for a full class with a non‑empty waitlist When a sandbox user injects a cancellation event for that attendee via controls Then the attendee is moved to canceled status and capacity increases by 1 And the next eligible waitlisted customer is auto‑promoted immediately And a sandbox payment prompt is issued to the promoted customer And all steps (cancellation, capacity change, promotion, payment request) are logged with timestamps and actors in the audit trail
No‑Response Expiration and Next‑In‑Line Promotion
Given a waitlisted customer has been auto‑promoted and a payment hold is active When the configured hold window elapses without payment Then the hold is marked expired and the customer returns to waitlist or is removed per policy And the next eligible waitlisted customer is auto‑promoted within 2 seconds of expiration And the sandbox records promotion notice and expiration events in the audit trail with precise timestamps And no more than one active hold exists for the released seat at any time
Class Time Change With Active Waitlist and Holds
Given a class with active waitlist entries and at least one active payment hold When the organizer changes the class start time in sandbox Then all affected waitlisted and held customers receive a sandbox reschedule notification And existing holds remain valid with the remaining time carried over unless policy requires re‑confirmation And if a promoted customer declines due to time change, the hold is released and the next eligible customer is auto‑promoted And the audit trail captures the time change, notifications issued, declines (if any), and subsequent promotions
Fast‑Forward Timers and Event Injection Controls With Audit Trail
Given pending timers exist (e.g., payment hold countdowns) When the sandbox fast‑forward control is used to advance time by a specified amount Then all time‑dependent triggers that would have fired in that interval execute immediately in correct order And when an event (e.g., manual seat release, test payment failure) is injected, the system processes it and updates state accordingly And the audit trail records the fast‑forward action, simulated current time, events fired, and resulting state changes And no real notifications or charges are sent; all communications are logged as sandbox artifacts
Reservation and Conflict Rules Emulation
"As an instructor managing multiple classes, I want sandbox reservations to enforce the same conflict and capacity rules so that I can confirm double-booking prevention works before publishing."
Description

Emulate production-grade reservation logic in sandbox, including capacity constraints, hold timers, double-booking prevention, resource conflicts (rooms, equipment, instructors), and cross-schedule linking. External calendar integrations are stubbed to avoid publishing test events while preserving conflict checks. The sandbox must honor the same validation messages, timing, and race conditions as live to ensure that fixes found in testing translate to production without regressions.

Acceptance Criteria
Sandbox Capacity, Hold Timer, and Waitlist Entry Parity
Given Test‑Drive Checkout sandbox is enabled and a class has capacity C and production-configured hold duration H And there are C−1 confirmed attendees When two users start checkout for the final seat Then the first atomic reservation places a hold for exactly H seconds (±1s) and decrements available seats to 0 And the second user is offered waitlist placement with the same message text/code as production And the held seat is visible as “Reserved (Hold)” in admin with a sandbox flag And availability and counters match production for the same inputs
Hold Expiry and Waitlist Auto‑Fill to Payment
Given a seat is held for User A with remaining hold time t and User B is first on the waitlist When the hold for User A expires without payment Then the seat returns to inventory within 1 second and is auto‑held for User B for H seconds And User B can complete payment to confirm; on failure or timeout, the seat returns and the next waitlisted user is auto‑held And state transitions and timestamps (hold→release→next hold) match production ordering and delays
Concurrent Reservation Contention and Double‑Booking Prevention
Given a user has an existing confirmed booking or active hold overlapping time window T When the user attempts another reservation that overlaps T in sandbox Then the attempt is blocked with the same validation message and error code as production And no duplicate charges or additional holds are created for the user And when two users contend for the last seat nearly simultaneously, only one confirmation succeeds and the other receives a sold‑out/seat‑lost message at the same step as production, based on the identical contention ordering rule
Resource Conflict Emulation (Rooms, Equipment, Instructors)
Given two or more sessions utilize the same instructor I, room R with capacity r, or equipment pool E with size e When pending holds and/or confirmations would exceed I, r, or e availability during overlapping times Then sandbox blocks the new hold/confirmation with the corresponding resource‑conflict message (instructor/room/equipment) identical to production And equipment counts decrement on hold and confirmation and are restored on hold expiry/cancel, matching production rules And the admin conflict calendar shows identical availability and conflict indicators as production
Cross‑Schedule Linking and Conflict Propagation
Given a pass or booking action auto‑enrolls the user into a linked series of sessions S1…Sn across schedules When the user attempts the reservation in sandbox Then conflicts across all linked sessions are evaluated atomically before confirmation And if any conflict exists, the entire operation is rejected with an aggregated message listing conflicting sessions (order and format identical to production) and no partial enrollments occur And if no conflicts exist, all linked holds/confirmations are created with the same timing and ordering as production
External Calendar Integrations Stubbed with Conflict Checks Preserved
Given Google/Outlook calendar integrations are enabled for the account When a sandbox hold or confirmation occurs Then zero live API calls are made to external calendar providers (only stub endpoints invoked) and no external events are created/updated/deleted And conflict checks use the stubbed provider data snapshot so existing external events still cause conflicts identical to production evaluation And audit logs record provider, operation, and correlation IDs with a "Stubbed" indicator using the same field names as production
Validation Message and Error Code Parity
Given any sandbox validation failure (capacity full, double‑booking, resource conflict, payment‑required, timer‑expired) When the error is returned to client or admin UI Then the message key, HTTP status, error code, parameter placeholders, and EN locale string exactly match production for the same condition And relative time text for holds/expiry uses the same rounding rules as production And analytics events emitted for the error have identical names and properties with sandbox=true
Test Data Isolation and Reset Controls
"As an operations manager, I want isolated test data with easy reset and seeding so that I can run repeatable tests without contaminating reports or live systems."
Description

Store all sandbox entities (customers, reservations, payments, logs) in a dedicated test partition that is excluded from analytics, payouts, CRM syncs, marketing automations, and exports by default. Provide admin controls to seed sample data, clone configurations from production, and reset the sandbox to a clean state with one click. Include an audit log of resets and seeds for compliance. Ensure PII handling is GDPR-safe with synthesized or anonymized defaults for sample attendees.

Acceptance Criteria
Sandbox Data Isolation From Production and Downstream Systems
Given sandbox mode is enabled for an organization When users create customers, reservations, payments, and logs in the sandbox Then all records are written to a dedicated test partition and tagged environment="test" And those records are excluded by default from analytics dashboards, payouts, CRM syncs, marketing automations, and public exports/APIs And production UI lists do not display sandbox records unless an admin enables a "Show test data" toggle for the current session And public/webhook integrations are disabled or routed to test endpoints while in sandbox And attempts to use live payment credentials in sandbox are blocked with a clear error and no charge created
Admin One-Click Reset to Clean Test State
Given an admin is in the sandbox environment When the admin initiates "Reset Sandbox" Then a confirmation modal appears requiring the admin to type RESET to proceed And upon confirmation, all sandbox entities (customers, reservations, waitlists, payments, logs, webhook deliveries, files) are deleted while configuration/settings are preserved And the operation provides a progress indicator and completes within 5 minutes for ≤100k entities And the system cancels in-flight sandbox jobs and suppresses outbound webhooks during the reset And an immutable audit entry is recorded with actor, timestamp, IP, counts before/after, and result status
GDPR-Safe Sample Data Seeding
Given an admin selects "Seed Sample Data" in sandbox When the seeding job runs Then all generated attendees/customers use synthesized PII (e.g., example.com emails, fake names, non-routable phone patterns) with data_origin="synthetic" And no real payment PANs are stored; payments use payment provider test tokens only And the admin can choose volume presets (10, 100, 1000) and locale for realistic formatting And the job writes an audit entry with actor, timestamp, parameters, and counts And seeding is blocked in production environments with an error message
Configuration Clone From Production to Sandbox (Secrets Sanitized)
Given an admin starts "Clone From Production" to sandbox When the clone runs Then only configuration objects are copied (classes, schedules, pricing, taxes, coupons, resources, policies, branding, notification templates) and no customer or transactional data is copied And all credentials, API keys, OAuth tokens, and secrets are excluded or replaced with test placeholders And external integration endpoints are switched to sandbox/test equivalents And new objects receive new IDs to avoid collisions while preserving referential integrity And a summary report shows objects cloned, skipped, and any warnings; an audit log entry is created
Exclusion of Sandbox Data From Exports and Reporting Interfaces
Given a user with report/export access generates analytics or exports When the "Include test data" option is not explicitly enabled by an admin Then sandbox data is excluded from analytics dashboards, CSV exports, BI connectors, and API exports by default And the UI displays a "Test data excluded" badge on affected views And if an admin enables includeTest, the output is clearly labeled [TEST], requires elevated permission, and the selection is session-scoped And scheduled syncs to CRM/marketing tools never include sandbox data And sandbox data is never included in payout calculations
Compliance Audit Log for Seeds, Clones, and Resets
Given any seed, clone, or reset action in sandbox When the action completes or fails Then an immutable audit record is appended with action type, actor ID, role, timestamp (UTC ISO 8601), IP, user-agent, requestId, parameters, counts before/after, result status, and error details if applicable And audit entries are read-only, tamper-evident, retained for at least 24 months, and exportable (CSV/JSON) by org admins And the audit log UI and API support filtering by action type, actor, date range, and result status And audit views redact any PII beyond synthesized placeholders
Mock Notifications and Receipts Inbox
"As a marketer, I want to preview all sandbox emails and SMS with real content so that I can validate branding and links without contacting real clients."
Description

Intercept and render all sandbox-triggered communications (email, SMS, and receipts) in an in-app inbox or safe email endpoints, ensuring no messages reach real customers. Messages display final templates with populated variables, attachments (e.g., PDF invoices), sender identity, and unsubscribe controls as they would appear in production. Provide delivery event simulations (queued, delivered, bounced) and allow download of artifacts for review. Integrates with branding settings so teams can verify tone, layout, and links before launch.

Acceptance Criteria
E2E Coverage Across Reservation, Waitlist Auto‑Fill, and Payment
Given a tester runs the Test‑Drive Checkout flow in sandbox mode When a reservation is created, a waitlist promotion occurs, and a payment succeeds with test cards Then a reservation confirmation, a waitlist promotion notification, and a payment receipt are each captured in the Mock Inbox And each message contains the correct booking data (client name, class, date/time with timezone, location/URL) And messages appear in chronological order with consistent threading/association to the same booking And no external delivery to real recipients occurs for any of the messages
Sandbox Interception of All Outbound Notifications
Given a workspace is in Test‑Drive Checkout sandbox mode When any booking flow action triggers an email, SMS, or receipt Then no request is sent to production delivery providers or real recipient endpoints And the message is captured in the in‑app Mock Inbox with environment set to sandbox And outbound recipient fields are masked in delivery calls and preserved as metadata (original vs routed) And an audit log entry records the interception with trigger, actor, and timestamp
Production‑Fidelity Message Rendering
Given a sandbox message is opened in the Mock Inbox When the message is rendered Then all template variables are fully populated using the sandbox booking data And branding settings (logo, colors, typography, sender name/from/reply‑to) are applied And unsubscribe/opt‑out controls are present per channel and positioned as in production And all links and CTAs resolve to the configured environment base URL And the rendered HTML/text output matches the production template engine output for the same inputs
Attachments and Artifact Download
Given a sandbox message includes an attachment such as a PDF invoice When the message is viewed in the Mock Inbox Then the attachment is listed with filename, MIME type, and size And authorized users can download the attachment successfully And the PDF invoice content includes correct branding, recipient details, itemization, taxes, totals, and booking references And the downloaded file bytes match the stored attachment (checksum equality)
Delivery Event Simulation and Status Timeline
Given a captured sandbox message When a tester simulates a delivery state of queued, delivered, or bounced Then the message status updates accordingly with a precise timestamp And for bounces, a selectable reason code and diagnostic text can be set and displayed And the status timeline records all state transitions in order And sandbox webhooks/events are emitted to connected test integrations as they would be in production
Safe Endpoints and Recipient Allow‑List
Given sandbox mode is active When a message targets a recipient not on the configured allow‑list Then the destination is rewritten to a configured safe email domain or test SMS number And the original intended recipient is preserved and displayed in the Mock Inbox metadata And attempts to send to non‑safe endpoints are blocked from external delivery and logged And administrators can configure the allow‑list and safe endpoints per workspace
Mock Inbox Access, Listing, and Filters
Given a team member with the required permission opens the Mock Inbox When viewing and managing messages Then only messages from their workspace’s sandbox are visible And messages can be filtered by channel, trigger type, status, date range, recipient, and booking ID And free‑text search returns messages by subject, body, or metadata And a secure preview link can be copied to share the message with teammates

Launch Guard

A preflight checklist that flags time‑zone mismatches, capacity conflicts, missing prices, and broken links. Choose a soft‑launch to a pilot group, then go live with one click and built‑in rollback for safe, confident publishing.

Requirements

Time-Zone Mismatch Detection
"As an instructor, I want Launch Guard to detect time-zone mismatches before publishing so that my students see accurate class times in their locale."
Description

Scan all scheduled sessions for inconsistencies across account default timezone, instructor timezone, venue timezone, and attendee-facing display. Normalize to IANA zones and highlight DST transition risks. Provide a preview of student-facing times by locale, suggested corrective actions (e.g., align to venue zone), and configurable severity levels. Block publishing on critical mismatches and allow inline fixes and re-validation without leaving the preflight.

Acceptance Criteria
Normalize Time Zones to IANA Canonical IDs
Given sessions and related entities contain time zone values in mixed formats (abbreviations, GMT offsets, legacy names) When Launch Guard runs Time-Zone Mismatch Detection Then each time zone for account default, instructor, venue, and attendee-facing display is normalized to a valid IANA identifier And any invalid or unresolvable time zone value is flagged as Critical And the normalized IANA identifier is displayed alongside the original input for review
Cross-Context Mismatch Detection and Severity Assignment
Given a schedule where account, instructor, venue, or attendee-facing display time zones are not aligned When validation runs Then mismatches are identified per session with the compared contexts and the computed offset in minutes And default severities are applied (e.g., venue vs instructor/session mismatch = Critical; account vs session mismatch = Warning; attendee display policy inconsistency = Warning) And severity labels appear per issue and contribute to an overall pass/fail summary
DST Transition Risk Highlighting
Given a session occurs on a date/time affected by a Daylight Saving Time transition in any involved time zone When validation runs Then sessions in ambiguous or nonexistent local times are flagged as Critical with the affected zone(s) identified And sessions within 24 hours of a DST change are flagged as Warning with the before/after offset change shown And the UI highlights the specific occurrence time in both pre- and post-transition contexts
Locale-Based Student Time Preview
Given a set of target attendee locales (system defaults or user-selected) When viewing the preflight preview Then the student-facing date and time render correctly for up to 20 locales with appropriate local time zone, DST indicator, and locale-specific formatting (12/24h) And previewed times reflect any in-session fixes applied during the current preflight state And the preview loads within 500 ms for the first 10 locales on a warm cache
Suggested Corrective Actions with Inline Fix and Immediate Re-Validation
Given one or more detected mismatches for a session When the user opens the issue details Then at least one context-aware corrective action is suggested (e.g., align instructor time zone to venue, align attendee-facing display to venue, update account default) And applying a suggested fix updates the underlying session setting without leaving the preflight view And validation automatically re-runs within 2 seconds and updates the issue list and previews
Block Publish on Critical Mismatches
Given one or more Critical issues remain after validation When the user attempts to publish Then the Publish action is disabled and a banner lists all blocking issues with deep links to fix each And once all Critical issues are resolved and validation passes, the Publish action becomes enabled And if severity configuration allows override for a Critical type, an explicit acknowledgment control appears and must be used to proceed
Severity Configuration Management
Given an administrator updates severity mappings for mismatch types in settings When the changes are saved Then subsequent validations immediately use the new severities without requiring a page reload And the configuration change is persisted and can be restored to defaults And historical validation results remain unchanged while future runs reflect the updated mapping
Capacity Conflict Detection
"As a studio owner, I want conflicts and double-bookings flagged with fixes so that I can prevent overcapacity and resource clashes."
Description

Detect overlapping bookings and overcapacity across instructors, rooms, and equipment, factoring setup/teardown buffers and resource constraints. Validate that session capacity aligns with membership/pack limits and parallel events. Offer auto-resolve options (adjust capacity, swap room, shift start time) and surface the impact on waitlists. Provide a hard-stop for critical conflicts and a warning for soft conflicts, fully integrated with the scheduling calendar to prevent double-booking.

Acceptance Criteria
Hard-Stop on Double-Booked Instructor with Buffer Overlap
Given Instructor I1 has a scheduled session from 09:00–10:00 with a 10-minute teardown buffer And Launch Guard preflight is run for a new session for I1 starting at 10:05 When the system validates instructor availability including buffers Then it detects a 5-minute overlap with the teardown buffer And marks the conflict type as InstructorOverbooked with severity=Critical and blocking=true And disables Save and Go Live actions And displays auto-resolve options: Shift start to 10:10, Assign different instructor And the conflict is cleared only when a resolution is applied
Room Capacity Hard-Stop and Auto-Adjust
Given Room Studio A has physical capacity=20 And a session is configured with capacity=25 in Studio A When Launch Guard validates capacity versus room constraints Then it raises a hard-stop conflict type=OverCapacity with severity=Critical and blocking=true And offers auto-resolve: Adjust capacity to 20 And shows projected waitlist impact as numeric counts (promotions/demotions) computed from current bookings and waitlist And blocks Save/Go Live until capacity<=20 or a different room is selected
Equipment Constraint Across Parallel Events
Given Equipment Spin Bikes available count=10 at the location And Session A (09:00–10:00) uses Spin Bikes with capacity=8 And Session B (09:15–10:00) uses Spin Bikes with capacity=6 When Launch Guard validates resource constraints across parallel events Then it detects 8+6>10 and raises conflict type=ResourceOverAllocated with severity=Critical and blocking=true And blocks Save/Go Live for Session B And offers auto-resolve options: Reduce capacity to 2, Swap to room with >=14 bikes, Shift start to first conflict-free time And shows the waitlist impact for each option as numeric counts before confirmation
Membership/Pack Limit Validation Across Parallel Events
Given Membership Gold Pack has max concurrent seat allocation=10 per time slot across the studio And existing sessions at 18:00 reserve a total of 7 seats for Gold Pack And a new 18:00 session reserves 5 seats for Gold Pack When Launch Guard validates membership/pack allocations Then it flags conflict type=MembershipAllocationExceeded with overage=2 and severity=Soft (non-blocking) And allows Save/Go Live to proceed with a visible warning And offers auto-resolve: Reduce reserved seats to 3 And displays the impact on waitlists for Gold Pack as numeric counts
Auto-Resolve: Swap Room Suggestion with Waitlist Impact
Given a session has a critical conflict due to room capacity or equipment shortage When the user selects Auto-Resolve option Swap Room Then the system lists only rooms that are free for the session window including setup/teardown buffers And displays for each suggestion: room name, capacity, equipment counts per type, and conflict resolution status And selecting a suggestion updates the session room and revalidates conflicts to None And shows waitlist impact (promotions/demotions) prior to confirmation And Save/Go Live become enabled after confirmation
Auto-Resolve: Shift Start Time Honoring Buffers
Given a session conflicts due to overlapping buffers on instructor, room, or equipment When the user selects Auto-Resolve option Shift Start Time Then the system proposes the earliest conflict-free start within the configured shift window And the proposal honors all setup/teardown buffers and resource availability And upon acceptance, the session start time is updated and all conflicts revalidate to None And a change summary displays the time delta and any waitlist impact
Calendar Integration: Block Critical Conflicts, Warn on Soft Conflicts
Given Launch Guard detects conflicts for a session When the user attempts to Save or Go Live Then if any conflict severity is Critical, the action is blocked and no calendar entry is created or modified And if only Soft conflicts exist, the action proceeds with a visible warning badge and an audit log entry of acknowledgment And in all cases, the scheduling calendar view reflects conflict status icons in real time And double-booking of instructors, rooms, or equipment cannot be created via drag-and-drop or API
Pricing Completeness Validator
"As an instructor, I want the system to verify pricing and payments are configured so that prospects can purchase without friction at launch."
Description

Verify that each published session has at least one purchasable option (drop-in, pack, membership), with currency, taxes/fees, and payment processor connectivity configured. Check discount code validity windows, visibility by channel (soft-launch vs public), and presence of refund/cancellation policy links. Present direct links to remediate gaps and block publishing when no viable purchase path exists.

Acceptance Criteria
Block Publish When No Purchasable Option Exists
Given a session is marked for publishing And it has zero active purchasable options (drop-in, pack, or membership) Or all options are invalidated by configuration errors When the user runs Launch Guard or clicks Publish Then the Publish action is disabled And a blocking error "No viable purchase path" is shown for the session And each issue lists a direct "Go to Pricing" link targeting the missing configuration And the session remains in Unpublished state
Payment Processor and Currency Validation
Given at least one price option exists for the session When Launch Guard validates pricing Then each option has a currency specified And all options for the session use the business default currency or a single session currency And a connected payment processor account with charges_enabled = true is present If any check fails, a blocking error is displayed with a "Connect Payments" or "Edit Currency" link And publishing is blocked until resolved
Taxes and Fees Configuration Completeness
Given at least one price option exists When Launch Guard validates tax and fee settings Then an applicable tax profile or explicit tax rate is assigned to the session or business And service/platform fee settings are defined for the channel And the final buyer total can be computed without ambiguity If missing or conflicting, a blocking error is shown with a link to Tax & Fees settings And publishing is blocked
Discount Code Validity and Applicability
Given one or more discount codes are attached to the session When Launch Guard validates discounts Then each code’s validity window includes the current time And usage limits (per-code and per-customer) have not been exceeded And each code applies to at least one active price option visible in the chosen channel If any code fails, the session cannot be published And each failing code lists an "Edit or Remove Code" link
Channel Visibility Consistency for Soft-Launch vs Public
Given the user selects a publication channel When channel = Public Then the session has at least one price option with visibility = Public And options restricted to Soft-Launch do not satisfy the requirement When channel = Soft-Launch Then the session has at least one price option with visibility = Soft-Launch And a pilot audience list is defined If the requirement for the selected channel is not met, publishing is blocked with a remediation link
Refund and Cancellation Policy Links Presence and Reachability
Given the session is prepared for publishing When Launch Guard checks policy links Then refund policy URL and cancellation policy URL are present at either session or business level And both URLs return HTTP 200–299 within 3 seconds If any link is missing or unreachable, a blocking error is shown with a "Add Policy" link And publishing is blocked until fixed
In-Context Remediation and Re-Validation
Given one or more blocking errors are detected When the user clicks a remediation deep link Then the user is navigated to the precise settings subsection for the failing field And upon save, the user can click "Re-check" in Launch Guard without leaving the session And if all checks pass, the "Go Live" button becomes enabled And a success toast confirms readiness to publish
Link Integrity Scanner
"As a marketer, I want broken or insecure links identified so that our launch pages and emails don’t send users to dead ends."
Description

Crawl and validate all hyperlinks used in event descriptions, instructor bios, assets, buttons, and transactional emails. Check HTTP status codes, redirect chains, SSL, and UTM parameter integrity; flag broken, slow, or non-HTTPS targets. Support whitelisting of private or environment-restricted links and provide inline edit and one-click recheck to confirm fixes before launch.

Acceptance Criteria
Full Content Crawl Coverage
Given a pending launch in Launch Guard containing event descriptions, instructor bios, asset/link fields, CTA buttons, and transactional email templates When the Link Integrity Scanner runs Then it extracts all http, https, mailto, tel, and app-deeplink schemes present in those sources And it produces a deduplicated inventory of links with source metadata (content type, record ID, field/element, display text) And it includes links from dynamically generated buttons and rich-text fields And it returns counts per source and a total count
HTTP Status and Redirect Validation
Given the scanner has an inventory of http/https links When validating each link Then it performs an HTTP HEAD request, falling back to GET if HEAD is not allowed And it follows redirects up to 5 hops and records the full redirect chain And it marks a link as Pass if the final status is 2xx And it marks a link as Fail if any terminal status is 4xx or 5xx or if the redirect chain exceeds 5 hops And it records final URL, final status code, and redirect count for reporting
TLS/HTTPS Enforcement
Given a scanned link When validating transport security Then http:// links are flagged Warn: Non-HTTPS Target unless matched by an HTTP-allowed whitelist rule And https:// links must negotiate TLS 1.2+ with a valid certificate (hostname, chain, not expired), else marked Fail
UTM Parameter Integrity
Given a scanned link contains UTM parameters When validating query parameters Then only utm_source, utm_medium, utm_campaign, utm_term, utm_content are permitted; unknown utm_* keys are flagged Warn And all UTM values must be non-empty and URL-encoded; empty or duplicate UTM keys are marked Fail And after following redirects, the final URL retains the original UTM parameters in full unless a whitelist rule allows stripping; otherwise mark Warn: UTM Stripped
Slow or Unreachable Target Detection
Given a link is being requested When measuring performance Then the request timeout is 10 seconds total and DNS/connect timeout is 5 seconds And links with time-to-first-byte > 1000 ms or total response time > 3000 ms are flagged Warn: Slow Target with measured timings And links that time out, fail DNS, or reset connections are marked Fail: Unreachable with error detail
Configurable Whitelisting of Private/Env-Restricted Links
Given a whitelist of domains, URL patterns, and expected statuses is configured When a scanned link matches a whitelist rule Then the scanner either skips the network request and marks the link as Whitelisted-Skipped or treats 401/403/451 as Pass-Expected per rule And whitelisted http:// or non-public hosts (e.g., localhost, 10.0.0.0/8) do not generate Warn/Fail severities And all whitelist decisions are logged with matched rule ID for audit
Inline Edit and One-Click Recheck
Given a link is flagged with Warn or Fail in Launch Guard results When the user edits the URL inline and clicks Recheck Then the system saves the change, re-validates only the edited link within 3 seconds, and updates the status in place And the new result overwrites the prior severity for that link and appends a timestamped check history entry And if the link passes, the issue count decrements and the item leaves the failure list automatically
Soft-Launch Cohort Targeting
"As a studio owner, I want to soft-launch to a private cohort so that I can validate details and gather feedback before going public."
Description

Enable selection or creation of pilot audiences from tags, membership tiers, past attendees, geolocation, or uploaded emails. Generate private access links or gated landing pages with an optional password. Schedule the soft-launch window, send invite templates, and track opens, clicks, and conversions. Allow rapid iteration on content, pricing, and capacity during soft-launch before escalating to public availability.

Acceptance Criteria
Cohort Builder Supports Tags, Tiers, History, Geolocation, and Uploads
Given a user selects one or more tags, membership tiers, past events for attendee history, a geolocation center and radius, and/or uploads a CSV of emails When they click Build Cohort Then the system merges all selected sources, normalizes and deduplicates contacts by email (case-insensitive), validates email formats, flags invalid rows with line numbers, and displays total and per-source counts. Given the user saves the cohort with a name When saved Then the cohort is reusable, editable, and shows created_at and last_updated timestamps. Given a geolocation filter is applied When the cohort is built Then only contacts whose latest known location falls within the specified radius of the chosen point are included.
Private Access Link and Gated Landing Page
Given a soft-launch cohort exists When the organizer generates a private access link Then the system creates a unique, unguessable URL and supports setting an optional password for the gated landing page. Given a visitor opens the private link When they authenticate with an email that is a member of the cohort and (if enabled) enter the correct password Then they can view and purchase the soft-launch offering; otherwise they see an access denied screen and the item remains hidden from public search and sitemaps. Given repeated failed password attempts (five or more within 10 minutes) When they occur Then access is rate-limited for 15 minutes and the attempts are logged.
Soft-Launch Window Scheduling and Enforcement
Given the organizer schedules a start and end date/time and selects a timezone When saved Then the system stores times in UTC and displays them in the account timezone; if the class/session uses a different timezone, a warning is displayed before saving. Given the private link is accessed before the start time When accessed Then a coming-soon message with the local start time is shown and booking is disabled. Given the private link is accessed during the soft-launch window When accessed Then only cohort members can view and book. Given the private link is accessed after the end time and the offering is not public When accessed Then access is disabled and a message indicates the soft-launch has ended.
Invite Campaign Delivery and Tracking
Given an invite template and a target cohort When the organizer sends the campaign Then emails are sent to all eligible recipients, with do-not-contact and unsubscribed members automatically excluded. Given tracked links in the invite When recipients interact Then delivery, open, click, bounce, and unsubscribe events are recorded per recipient. Given a campaign has sent When the organizer opens the report Then they can see aggregate metrics (delivered, open rate, click rate) and export a per-recipient CSV including delivered_at, opened_at, clicked_at. Given a recipient clicks unsubscribe When processed Then the contact is suppressed from future emails and the campaign report reflects the unsubscribe.
Conversion Attribution for Soft-Launch
Given a recipient clicks a tracked campaign link or visits via the private link and completes a paid booking during the soft-launch When payment is confirmed Then the system marks the contact as Converted and attributes the conversion (last-click for email; direct-cohort for private link) with order ID, timestamp, and revenue. Given the same contact makes multiple purchases during the soft-launch When subsequent purchases occur Then total revenue and number of conversions are updated without double-counting the contact in the unique conversion count. Given the organizer views cohort analytics When opened Then conversion rate (%) and total revenue are displayed for the soft-launch.
Rapid Iteration During Soft-Launch
Given the soft-launch is active When the organizer edits content (title, description, media) Then the landing page and checkout reflect changes within 60 seconds. Given the organizer changes pricing When saved Then new bookings use the new price, existing orders remain unchanged, and the price change is recorded in an audit log with timestamp and user. Given the organizer changes capacity When reduced below current bookings Then the system blocks the change with a clear error; when increased, the new capacity is enforced without overselling. Given the organizer enables A/B variants (up to two) When saved Then the cohort can be split evenly or by rule, and metrics (views, clicks, conversions, revenue) are tracked per variant.
One-Click Escalation to Public and Rollback
Given a soft-launch is ready for public release When the organizer clicks Go Public Then the listing becomes discoverable via public search and sitemap, cohort gating is removed, and private links redirect to the public page. Given the organizer clicks Rollback after going public When confirmed Then the listing returns to the prior soft-launch state (public visibility removed, cohort gating restored), existing bookings remain valid, and all changes are logged. Given multiple publish or rollback actions occur When they happen Then no duplicate listings are created and the audit log shows an ordered history of each action with user and timestamp.
One-Click Go-Live with Rollback
"As a studio owner, I want one-click go-live with rollback so that I can publish confidently and recover quickly from mistakes."
Description

Provide an atomic publish action that promotes a validated draft to live, capturing a versioned snapshot of settings, content, pricing, and visibility. Support one-click rollback to the prior stable version with options to preserve or cancel bookings made since publish per policy. Include idempotent retries, alerting on failure, and a complete audit log of publish and rollback events.

Acceptance Criteria
Atomic Publish Promotes Validated Draft
Given a draft version has passed all preflight validations and no publish is currently in progress When the user clicks Go Live Then the system executes a single atomic transaction that either fully applies the draft or leaves the live state unchanged And Then the live catalog reflects the draft’s settings, content, pricing, and visibility within 2 seconds And End users and APIs do not observe any intermediate state during the operation
Versioned Snapshot Captured on Publish
Given a draft is successfully published Then an immutable snapshot is created with a unique version ID, UTC timestamp, actor ID, checksum, and reference to the prior version And The snapshot stores settings, content references, pricing rules, visibility flags, and feature toggles used to render live And The live pointer is updated to the new version ID and is readable via the versions API
Idempotent Publish Retries Without Duplication
Given a transient infrastructure error occurs during publish When the system retries up to 3 times within 60 seconds using the same idempotency key Then at most one live version is created and marked as current And Only one set of side effects is emitted (notifications, webhooks, cache invalidations) And If retries exhaust, no changes are applied and an error is returned with a correlation ID
One-Click Rollback to Prior Stable Version
Given the current live version is Vn and a prior stable version Vn-1 exists When the user clicks Rollback from the admin UI and confirms Then the system atomically switches the live pointer to Vn-1 within 2 seconds And All live reads immediately resolve to Vn-1 with zero partial state exposure And Preconditions (e.g., referenced resources still exist) are validated; unmet preconditions block rollback with a descriptive error
Rollback Booking Policy Handling (Preserve or Cancel)
Given bookings were created after the latest publish And The user selects either Preserve bookings or Cancel and refund per policy When rollback executes Then the chosen policy is applied consistently to all affected bookings And Under Preserve bookings, bookings remain active and are mapped to equivalent sessions in Vn-1; unresolvable mappings are queued for manual review without automatic cancellation And Under Cancel and refund, affected bookings are canceled, refunds are issued per payment policy within 15 minutes, and attendees receive notifications And No bookings created before the latest publish are modified
Audit Log for Publish and Rollback Events
Given any publish or rollback action is initiated Then the audit log records events publish_started, publish_succeeded/publish_failed, rollback_started, rollback_succeeded/rollback_failed with UTC timestamp, actor ID, version IDs (from/to), policy (if rollback), idempotency key, correlation ID, and failure reason (if any) And Audit entries are immutable, queryable by date range and correlation ID, and exportable as CSV/JSON And The audit log displays in the admin UI within 10 seconds of event completion
Failure Alerting and Operator Feedback
Given a publish or rollback fails or exceeds a 30-second SLA Then an alert is sent to configured channels (email and in-app) within 60 seconds including correlation ID, summary, and next-step guidance And The UI surfaces an actionable error state with a retry option and a link to audit details And Subsequent retries are rate-limited (max 3 per 5 minutes) and reuse the same idempotency key
Preflight Impact Summary & Blockers
"As an instructor, I want a clear preflight summary with blockers so that I know exactly what to fix to launch safely."
Description

Consolidate all checks into a severity-ranked report showing pass/fail status, impacted sessions/resources, and recommended fixes. Allow waiver of non-critical items with justification, and enforce configurable blockers that prevent publishing on critical failures. Provide an exportable/shareable summary and keep the report visible in both soft-launch and final publish screens.

Acceptance Criteria
Severity-Ranked Preflight Report with Pass/Fail and Impacts
Given a draft launch with multiple preflight checks across severities When the user opens the Preflight Impact Summary Then the report lists all checks sorted by severity (Critical > Major > Minor) and within each severity by status (Fail before Pass) And each check row displays: check name, severity, status (Pass/Fail), count of impacted items, and an expandable list of impacted sessions/resources with clear identifiers (e.g., Session Title, Date/Time, Resource/URL) And the report header shows total counts by severity and by status (e.g., Critical: 2 fails, 3 passes)
Recommended Fixes and Quick Actions for Failed Checks
Given any failed check in the preflight report When the user expands the check Then a recommended fix is displayed with concrete steps and a deep link to the relevant edit screen for the impacted item(s) And clicking the deep link navigates to the correct configuration with the impacted item pre-selected or filtered And upon saving a valid fix and returning to the report, the check status updates to Pass without a full page reload within 3 seconds
Non-Critical Waiver with Mandatory Justification and Audit Trail
Given a failed check whose severity is not Critical When the user clicks Waive Then a modal requires a justification of at least 10 characters before enabling Confirm And upon confirmation, the check is marked as Waived with a label/icon and displays waiver author, UTC timestamp, and justification on hover or expand And the waived check remains visible but no longer counts toward blocking publish; it shows status Fail (Waived) And the user can Unwaive to restore normal behavior; all waiver/unwaive actions are logged to the audit trail
Configurable Blockers Prevent Publish on Critical Failures
Given Blocker Configuration designates which severities/check types block publishing And at least one failed check matches the configured blockers When the user attempts Soft Launch or Final Publish Then the publish action is prevented: the buttons are disabled and an inline message lists the blocking check names and counts And API publish attempts return HTTP 409 with error code PRELAUNCH_BLOCKED and the IDs of blocking checks And when all configured blockers are resolved (no remaining blocking fails), the buttons re-enable within 2 seconds
Exportable and Shareable Preflight Summary
Given a preflight report is available When the user selects Export Then both PDF and CSV files are generated containing: report timestamp, environment (Soft Launch or Final Publish), overall counts, and each check’s severity, status, waiver state/justification, and impacted items And file generation completes within 5 seconds for reports up to 500 impacted items; filenames follow preflight-summary-YYYYMMDD-HHMM.{pdf|csv} When the user creates a Share Link Then a read-only URL with a 7-day expiring token is created; recipients can view the same content without authentication And the owner can revoke the link, immediately invalidating the token
Report Visibility and Consistency Across Soft-Launch and Final Publish
Given an event with a preflight report When the user navigates to the Soft-Launch screen Then the Preflight Impact Summary is visible above launch controls and reflects the current state When the user navigates to the Final Publish screen for the same event without changes Then the report content (counts, statuses, waiver states) is identical And after underlying data changes (e.g., fixing a price, updating a link), both screens show the updated report upon refresh or within 3 seconds of returning

Guardian Roles

Define primary and secondary guardians, pickup-only contacts, and emergency backups per child. Fine-grained permissions control who can book, pay, sign waivers, and receive communications. Reduces front-desk confusion and keeps kids safe with clear, auditable authorization.

Requirements

Child-Centric Guardian Profiles
"As a studio admin, I want to assign primary, secondary, pickup-only, and emergency guardians to a child so that staff know exactly who is authorized for each action."
Description

Enables admins to create and manage multiple guardian roles per child—primary, secondary, pickup-only, and emergency backup—with relationship labels, contact methods, verification status, and validity dates. Supports linking existing ClassTap accounts or creating profile-only contacts, sets role precedence for decision-making, and prevents duplicates with deduplication and merge rules. Ensures consistency across programs for the same child, exposes streamlined web/mobile UI for add/edit/suspend/revoke, and provides APIs for syncing with external SIS/CRM systems to keep records current.

Acceptance Criteria
Create and Manage Guardian Roles per Child
Given I am an admin viewing a child’s profile When I add guardian roles of types Primary, Secondary, Pickup-Only, or Emergency Backup with relationship label, at least one contact method (email or phone), verification status defaulted to Unverified, and validity start/end dates Then the roles are saved and immediately visible on the child profile with their permissions per role type And at most one Primary role is Active at any time per child (based on overlapping validity dates) And roles are Active only within their validity window and Inactive outside it And the configuration propagates to all of the child’s active and future program enrollments within 60 seconds And all changes are audit-logged with actor, timestamp, and before/after values
Link Existing Account or Create Profile-Only Contact
Given I search for a guardian by email, phone, or name while adding a role to a child When an exact match to an existing ClassTap account is found Then I can link that account to the child without creating a duplicate and the guardian sees the child in their account When no match is found or I choose Create Profile-Only Then the system creates a profile-only guardian with required fields (full name + at least one contact method) and no login access And the system prevents linking the same person twice to the same child by blocking duplicate email/phone for the same child-role pair And a profile-only guardian can be upgraded to a full account via invitation without breaking existing links
Verify Guardian Contact Methods
Given a guardian has an unverified email or phone When verification is initiated by admin or guardian Then a one-time code (6 digits) is sent and is valid for 10 minutes with a maximum of 5 attempts And upon successful entry, the contact method status becomes Verified with timestamp and verifier recorded And any permission gated by verification (booking, payment, waiver signature) remains blocked until at least one contact method for that guardian is Verified And failed or expired attempts are recorded and visible to admins
Enforce Role Precedence and Permissions
Given role precedence Primary > Secondary > Pickup-Only > Emergency Backup and role-based permissions are configured (e.g., who can book, pay, sign waivers, receive communications) When two guardians attempt conflicting actions within overlapping validity periods Then the higher-precedence guardian’s action is accepted and the lower-precedence action is denied with an on-screen message and audit entry citing precedence And guardians whose roles are Inactive (expired, suspended, or revoked) cannot perform protected actions And class communications are sent only to guardians with the Receive Communications permission for the child
Prevent Duplicates and Support Merge
Given I attempt to add a guardian for a child using an email or phone already associated to any guardian record in the tenant When a potential duplicate is detected by matching email or phone (or external_id when present) Then the system blocks creation and prompts me to Link existing or Merge When I choose Merge between two guardian records for the same person Then I must select the canonical record and confirm And contact methods, verification statuses, and role links to the child are combined without creating duplicate role entries And a complete merge audit (source, target, fields kept/overwritten, actor, timestamp) is recorded
Admin UI: Add/Edit/Suspend/Revoke Guardians
Given I am an admin on web or mobile When I Add or Edit a guardian role Then required fields are enforced with inline validation and a success state is shown on save When I Suspend a guardian role Then the role becomes Inactive until reactivated, without deleting history, and upcoming permissions are blocked When I Revoke a guardian role Then the role is immediately Inactive, future permissions are removed, historical records remain, and the role is labeled Revoked And all changes are reflected on the child profile and enrollment rosters within 60 seconds
SIS/CRM Sync API for Guardian Roles
Given an external SIS/CRM sends a request to POST/PUT /api/v1/guardian-roles with tenant context, child_id, role type, relationship label, contact methods, verification flags, validity dates, and external_id When the payload is valid Then the API performs an idempotent upsert keyed by tenant + external_id (or email/phone when configured) and returns 200 (existing) or 201 (created) with resource IDs And invalid or conflicting data returns 400/409 with machine-readable error codes and fields And the API can link to existing ClassTap accounts by email/phone without creating duplicates And all changes are processed within 60 seconds and recorded in an integration audit log
Permission Matrix & Role Policies
"As a primary guardian, I want clear permissions to book and pay for my child while restricting pickup-only contacts from making changes so that responsibilities are enforced and errors are avoided."
Description

Provides a configurable permission model mapping roles to capabilities such as booking classes, making payments, managing payment methods, signing waivers/consents, managing subscriptions/passes, viewing attendance/history, and receiving communications. Includes default policies per role with per-child overrides and effective-permission resolution when a guardian has multiple roles. Enforces permissions consistently across calendar booking, waitlist-to-payment flows, invoicing, waiver capture, and messaging via server-side checks and clear UI indicators explaining allowed and blocked actions.

Acceptance Criteria
Default Role Policy Applied on Assignment
Given a configured default policy for role "Secondary Guardian" with: And Book Classes = Allowed And Make Payments = Denied And Manage Payment Methods = Denied And Sign Waivers/Consents = Allowed And Manage Subscriptions/Passes = Denied And View Attendance/History = Allowed And Receive Communications = Allowed When Alex is assigned role "Secondary Guardian" for child Maya Then Alex's effective permissions for Maya match the policy above
Per-Child Override Precedence
Given Alex is "Secondary Guardian" for child Maya And the default policy denies Make Payments and Manage Payment Methods And an override for Maya sets Make Payments = Allowed When Alex attempts to pay an outstanding invoice for Maya via the payment link Then the payment is permitted (HTTP 201) and the invoice is marked paid When Alex attempts to add a new payment method for Maya Then the action is blocked (HTTP 403 PERMISSION_DENIED: ManagePaymentMethods) and no payment method is created
Effective Permission Resolution with Multiple Roles
Given Taylor has roles "Secondary Guardian" and "Pickup-Only" for child Rio And the default policy for "Secondary Guardian" allows Book Classes and denies Make Payments And the default policy for "Pickup-Only" denies all except Receive Communications And a per-child override for Rio sets Make Payments = Denied When computing effective permissions for Rio Then Book Classes = Allowed And Receive Communications = Allowed And Make Payments = Denied (per-child override takes precedence) And no capability not explicitly allowed by any role is Allowed When Taylor is additionally assigned role "Primary Guardian" for Rio Then Make Payments remains Denied due to the per-child override
Server-Side Enforcement Across Booking, Waitlist-to-Payment, Invoicing, and Passes
Given Casey lacks Make Payments for child Luna but has Book Classes When Casey books Luna into a free class from the calendar Then booking succeeds (HTTP 201) and attendance is created When Casey attempts to convert a waitlist spot to a paid booking for Luna Then the server returns HTTP 403 (PERMISSION_DENIED: MakePayments) and no invoice, charge, or enrollment is created When an administrator issues an invoice for Luna and sends Casey a payment link Then attempting payment via the link returns HTTP 403 (PERMISSION_DENIED: MakePayments) and the invoice remains unpaid When Casey attempts to purchase a pass or subscription for Luna Then the server returns HTTP 403 (PERMISSION_DENIED: ManageSubscriptionsPasses) and no pass or subscription is created
Waiver/Consent Capture Permission Enforcement
Given Priya has Sign Waivers/Consents = Allowed for child Noah And Alex has Sign Waivers/Consents = Denied for child Noah And a waiver is required before Noah's first class check-in When Priya opens the waiver form for Noah Then Priya can complete and submit the waiver (HTTP 201) and Noah's waiver status is set to Completed When Alex opens the waiver form for Noah Then the form is read-only with a message indicating lack of authorization And any attempt to submit returns HTTP 403 (PERMISSION_DENIED: SignWaivers) and no waiver record is created
Messaging Recipient Permission Enforcement
Given a class update is sent to guardians for child Zara And recipients must have Receive Communications = Allowed for Zara And Jordan has Receive Communications = Allowed for Zara And Emma has Receive Communications = Denied for Zara When the message is sent Then Jordan receives the message via configured channels (e.g., email, in-app) And Emma does not receive the message And the delivery report lists only recipients with Receive Communications = Allowed at send time
UI Indicators for Allowed and Blocked Actions
Given Sam is viewing child Olive's class detail page And Sam has Book Classes = Allowed and Make Payments = Denied for Olive When the action area renders Then the "Book Free Class" button is enabled And the "Pay Now" button is disabled with a lock icon and tooltip "Blocked by role policy: Make Payments is not allowed for Olive" And clicking the disabled "Pay Now" button shows an inline explanation modal and does not trigger any API call And a "Why?" link opens a panel listing Sam's effective permissions for Olive
Authorized Pickup Verification & Check-In
"As a front-desk staff member, I want to verify authorized pickup contacts at check-out so that children are released only to approved guardians."
Description

Delivers a secure pickup workflow validating authorized contacts at check-out using PIN codes, QR passes, or photo/ID verification, with time-bound authorization windows and optional two-factor confirmation to the primary guardian. Integrates with attendee lists and kiosk modes to prevent release to unauthorized contacts, records pickup attempts, and stores time-stamped check-in/out logs per child for auditability. Supports temporary pickup delegates with expiration and special-instructions notes for staff.

Acceptance Criteria
Kiosk QR Pickup Verification
Given a child is checked-in to a session and an authorized pickup contact holds an active QR pass When the contact scans the QR code at the kiosk Then the system validates the pass against the child's active authorization records and time window And displays the child's name, photo, and pickup permissions to staff And marks the child as picked up with a server time-stamped log (UTC and local) And prevents duplicate pickup by disabling further release attempts for that session And sends a pickup confirmation notification to the primary guardian
PIN + Photo ID Front-Desk Verification
Given a staff member initiates pickup for a child at the front desk When the contact provides the child's pickup PIN and a government-issued photo ID Then the system verifies the PIN matches an authorized contact and shows the stored guardian photo (if available) for visual match And requires staff confirmation of ID match via a checkbox with staff user ID captured And records the pickup with timestamp, verifying method "PIN+ID", and staff identifier And denies pickup if the PIN is incorrect after 3 attempts, locking for 2 minutes and logging failed attempts
Time-Bound Authorization Window Enforcement
Given an authorized contact attempts pickup When the current time is within the defined authorization window for that contact Then pickup is allowed and logged with "within window" flag When the current time is outside the window Then pickup is blocked with a clear reason displayed to staff And an override option is available only after two-factor approval by the primary guardian And all approvals/denials are time-stamped and linked to the approving guardian identity
Two-Factor Confirmation for New or Elevated-Risk Pickups
Given a pickup attempt by a contact flagged as "first-time" or "requires 2FA" When staff triggers two-factor confirmation Then the primary guardian receives a push/SMS with child name, contact name, and one-tap Approve/Deny And the system requires response within 5 minutes, displaying countdown to staff And on Approve, pickup proceeds and the approval is bound to this attempt only And on Deny or timeout, pickup is blocked and the event is logged and notified to the primary guardian
Temporary Delegate with Expiration and Special Instructions
Given a temporary delegate is authorized for a child with a start/end datetime and special instructions When the delegate attempts pickup before expiration and after start time Then the system allows pickup and displays the special instructions to staff prior to confirmation And records that the temporary delegate performed pickup with their verification method When the delegate attempts pickup after expiration Then the system blocks release and suggests contacting the primary guardian
Unauthorized Contact Attempt Logging and Alerts
Given a contact not listed as authorized, temporary delegate, or pickup-only contact attempts pickup When they provide any verification method (QR, PIN, ID) Then the system rejects the pickup with a clear reason "Unauthorized contact" And logs the attempt with timestamp, attempted method, entered identifiers (masked where applicable), and staff/kiosk ID And immediately notifies the primary guardian and secondary guardian via preferred channels
Comprehensive Check-In/Out Audit Log and Export
Given an admin views a child's attendance record When viewing the check-in/out log for a date range Then the system displays chronological entries including check-in, pickup method, verifier, contact identity, timestamps, and any override/2FA artifacts And entries are immutable and tamper-evident with an audit trail for any corrections And the log can be exported to CSV for a selected date range with the same fields And the exported file matches on-screen records by count and checksum
Waiver & Consent Delegation
"As a primary guardian, I want to sign waivers and designate who else can sign so that my child’s enrollment meets legal requirements without delays."
Description

Controls which guardians may sign required waivers and medical/emergency consents per child and program, capturing legally valid e-signatures with relationship and identity verification. Automatically prompts for re-consent on version changes, blocks participation until required signatures are on file, and stores signed artifacts with versioning for legal audit. Integrates with booking and enrollment so only signing-authorized guardians can complete checkout.

Acceptance Criteria
Authorized Guardian Signs Program Waiver
Given a child C enrolled in Program P that requires Waiver version vN And Guardian G has signing permission for C in P When G is authenticated and initiates the waiver signing flow for C in P Then the system displays Waiver vN content and required consent acknowledgments And G can apply an e-signature and confirm consent And the system records completion and marks Waiver vN as satisfied for C in P
Unauthorized Guardian Blocked from Signing
Given Guardian G does not have signing permission for child C in Program P When G attempts to access or complete the waiver/consent signing for C in P Then the system prevents signing and shows a "Not authorized to sign" message And no signature record is created And the attempt is logged with timestamp and user identifier
Identity & Relationship Verification During e-Sign
Given Guardian G is authorized to sign for child C in Program P When G proceeds to sign a required waiver/consent Then the system verifies identity via a one-time code sent to G's verified email or phone on file And G must confirm their relationship role (e.g., Primary, Secondary) which is captured with the signature And the signature cannot be completed if OTP verification fails or role is not provided And the verification method and outcome are recorded with the signature
Auto Re-Consent on Waiver Version Update
Given child C has a completed Waiver v1 for Program P And Program P publishes Waiver v2 When an enrollment or checkout for C in P is initiated after the publish time Then the system flags the consent as outdated and requires re-consent to v2 before participation And authorized signing guardians for C in P are notified to sign v2 And the signed v1 artifact remains preserved and is linked to the v2 requirement in the audit trail
Checkout Restriction Based on Signing Authorization
Given user U initiates checkout for child C in Program P where required consents are pending And U is not an authorized signer for C in P When U reaches the payment step Then the system blocks payment and displays a prompt to invite an authorized signer to complete required consents And checkout cannot be completed until required consents are signed by an authorized guardian And the blocked checkout attempt is logged
Per-Child, Per-Program Consent Enforcement
Given a family account has children C1 and C2 and Programs P1 and P2 each requiring waivers/consents When a waiver is signed for C1 in P1 Then only C1 in P1 is marked satisfied And C1 in P2 and C2 in any program remain pending until signed for those specific child-program combinations
Signed Artifact Versioning & Audit Evidence
Given a required waiver/consent is signed When the system stores the signed record Then the artifact includes: waiver version ID, content hash, signer full name, relationship role, UTC timestamp, IP address, user ID, verification method and outcome, and signature intent/image as applicable And the artifact can be retrieved and rendered exactly as signed (e.g., PDF) with its metadata And any new waiver text or setting changes create a new version and do not mutate the original artifact
Communication Routing & Preferences
"As a secondary guardian, I want to receive schedule updates but not billing messages so that I stay informed without seeing private payment details."
Description

Routes confirmations, reminders, schedule changes, and emergency alerts to appropriate guardians based on role and channel preferences, supporting multiple recipients and per-message visibility rules. Allows admins and guardians to configure email, SMS, and push preferences at the child and program level, consolidates duplicates to avoid spam, and suppresses sensitive billing details for pickup-only contacts. Manages bounces, opt-outs, and fallback routing to maximize delivery reliability.

Acceptance Criteria
Booking Confirmation Routed by Guardian Role and Preferences
Given a child has Primary and Secondary guardians with permissions configured and pickup-only/emergency-only contacts defined And given guardian channel preferences exist at both child and program levels When a booking is completed for that child in a specific program Then the system sends a booking confirmation to each guardian who has "receive confirmations" permission for that child/program And uses program-level channel preferences when present, otherwise falls back to child-level preferences And excludes pickup-only and emergency-only contacts from receiving confirmations And consolidates duplicate endpoints so each unique email/phone/device receives at most one confirmation for the event
Program-Level Preferences Override Child-Level Defaults
Given a guardian has channel preferences set at the child level And different channel preferences set for a specific program When any non-emergency message for that program (e.g., reminder, schedule change) is sent Then program-level preferences take precedence over child-level preferences for that message And for channels not explicitly configured at the program level, the system uses the child-level settings And if no preferences exist at either level, the system uses the organization default
Multi-Recipient Reminders with Deduplication and Visibility Rules
Given a class session reminder is due for a child with Primary and Secondary guardians who both have "receive reminders" permission And the guardians share the same email address while having distinct phone numbers When the reminder is sent Then the system sends one SMS to each guardian with SMS enabled And sends a single email to the shared email address while attributing delivery to both guardians And excludes pickup-only and emergency-only contacts from reminders And ensures no guardian receives duplicate reminders for the same event across identical endpoints
Sensitive Billing Details Suppressed for Pickup-Only Contacts
Given a billing-related communication (invoice, payment receipt, failed payment) is generated for a child When determining recipients and content Then pickup-only contacts do not receive any billing-related messages And emergency-only contacts do not receive any billing-related messages And eligible guardians (Primary/Secondary with billing permission) receive full billing details according to their channel preferences And if pickup logistics must be communicated to pickup-only contacts, a billing-free version is sent that contains only schedule/pickup information
Bounce, Opt-Out, and Fallback Routing for Non-Emergency Messages
Given a guardian is eligible to receive a schedule change notification via email and SMS And the guardian's email address hard-bounces or is marked undeliverable When the schedule change is sent Then the system suppresses future email sends to the bounced address And immediately falls back to another enabled channel for that guardian (e.g., SMS or push) And does not use any channel the guardian has explicitly opted out of And if all channels are unavailable or fail, the system records a non-delivery event and surfaces an admin alert for follow-up
Emergency Alerts with Fallback and Opt-Out Management
Given an emergency alert is triggered for a child currently enrolled in a session When routing the alert Then the system sends the alert to Primary, Secondary, and Emergency Backup contacts using all enabled emergency channels per guardian And excludes pickup-only contacts unless they have emergency alerts enabled And if a channel fails (bounce, timeout) or is opted out, the system immediately retries via any other enabled channel for that guardian and continues with other guardians And if no delivery succeeds for any guardian within 5 minutes, the system flags the incident for admin escalation and records all attempts
Admin Preview and Audit Log of Routing Decisions
Given an admin prepares to send a schedule change or reminder When opening the recipient preview Then the system displays intended recipients by guardian role and channel with reasons for inclusion/exclusion (permissions, preferences, deduplication, opt-outs) And after sending, the message log records per-guardian, per-channel outcomes (sent, delivered, bounced, suppressed) and the visibility rule applied And the audit log is exportable and filterable by child, program, guardian, message type, and date range
Authorization Audit Trail & Reporting
"As an owner, I want an audit log of who authorized, booked, paid, and picked up so that I can resolve disputes and demonstrate compliance."
Description

Captures an immutable audit trail of guardian-related actions including role assignments, permission changes, bookings, payments, waiver signatures, communications, and pickups, with timestamps, actor identity, IP/device metadata, and the affected child. Provides filters and exportable reports to resolve disputes and satisfy compliance, includes retention policies and privacy redactions, and surfaces inline explanations when actions are blocked by policy to increase transparency.

Acceptance Criteria
Log Guardian Role and Permission Changes
Given a privileged user updates a guardian’s role or permissions for a child When the update is committed Then an audit event is appended with fields: event_id (UUIDv4), event_type (role_assignment|permission_change), child_id, actor_id, actor_type (staff|guardian|system), target_guardian_id, changed_fields, previous_values, new_values, timestamp (ISO 8601 UTC ms), ip_address (IPv4/IPv6), user_agent, device_fingerprint (nullable), correlation_id (UUID) And Then the event is persisted atomically and visible in the child’s audit list within 2 seconds And Then duplicate submissions with the same correlation_id do not create duplicate events (idempotent)
Capture Key Guardian-Related Transactions
Given a guardian or staff completes or fails a booking, payment, waiver signature, communication send, or pickup confirmation affecting a child When the action result is determined (success or failed) Then an audit event is recorded with event_type in {booking_created, booking_cancelled, payment_authorized, payment_captured, payment_refunded, waiver_signed, communication_sent, pickup_confirmed, action_blocked} And Then each event includes child_id, actor_id, actor_type, related_entity_id (booking_id|payment_id|waiver_id|message_id|pickup_id), status (success|failed), communication_channel (email|sms|push|null), timestamp (ISO 8601 UTC ms), ip_address, user_agent, device_fingerprint (nullable), policy_reference (nullable) And Then failed actions include reason_code and message And Then events are ordered primarily by timestamp and secondarily by event_id for tie-breaks
Audit Log Immutability and Tamper Detection
Given any role attempts to update or delete an existing audit event via UI or API When the request is processed Then the operation is denied with HTTP 403 and a user-safe error message And Then a new audit event of type audit_access_denied is appended with actor_id, timestamp, ip_address Given storage of an audit event When the event is persisted Then its content_hash and previous_hash are computed to create a hash chain And Then any integrity check detecting a hash mismatch sets integrity_status = failed for the affected range and emits an integrity_alert event
Filtering, Sorting, and Pagination of Audit Logs
Given a staff user views the audit trail When filters are applied by child_id, guardian_id, event_type, class_id, date_range (UTC), actor_type, ip_address, and status Then results reflect all filters with inclusive date bounds And Then results can be sorted by timestamp (asc|desc) And Then pagination supports page_size up to 200 and returns total_count and next_page_token And Then queries returning up to 50,000 events complete in ≤ 2 seconds at the 95th percentile
Export Reports with Redaction and Compliance Metadata
Given a staff user exports the current filtered audit results When an export is requested Then CSV and JSON exports are generated with header metadata: export_id, generated_at (ISO 8601 UTC), organization_id, filter_summary, integrity_status And Then PII is redacted per viewer role: unauthorized viewers see masked email (first 2 chars + **** + domain), masked IP (first 2 octets + **.**), and masked phone (***-***-last4); authorized viewers see full values And Then exports over 100,000 rows run asynchronously with job status endpoints and a signed download URL that expires in 24 hours And Then exported record count and order match the on-screen results
Retention Policy, Purge, and Legal Hold
Given an organization retention period is configured When an event’s age exceeds the retention period and no legal hold applies Then the event is purged by a scheduled process And Then a purge_summary audit event is created with counts and date ranges of purged items And Then legal holds prevent purge until released, and changes to retention/holds are themselves audited And Then data subject exports include only non-purged events and preserve redactions
Blocked Action Explanations and Logging
Given a user attempts an action that is disallowed by guardian role or permission (e.g., booking without booking permission) When the action is blocked by policy Then the UI displays an inline explanation including policy name, required permission/role, and next steps, without exposing other children’s data And Then the API response includes HTTP 403/409 with reason_code and i18n message key And Then an action_blocked audit event is recorded with policy_reference, actor_id, child_id, timestamp (ISO 8601 UTC ms), ip_address, and reason_code

AgeSmart Eligibility

Automatically match children to age-appropriate classes based on date of birth, school grade, and prerequisites. Ineligible sessions are hidden or clearly labeled with reasons; upcoming birthdays trigger move-up suggestions. Prevents misbookings, saves staff time, and ensures kids land in the right level every time.

Requirements

Eligibility Rules Engine
"As a studio admin, I want to define and apply flexible eligibility rules so that only appropriately aged and qualified children can book each class."
Description

Implements a centralized, configurable engine that evaluates a child’s eligibility for each class using multiple criteria: date-of-birth age ranges (min/max), school grade, prerequisite completions, instructor assessments, and custom flags. Supports “age on date” policies (class start date, end date, or fixed cutoff), effective date windows (terms/seasons), AND/OR logic, rule precedence, and reusable rule templates. Delivers consistent, real-time eligibility results across search, class detail, cart, waitlist, and payment flows. Designed for low-latency evaluation and horizontal scalability, with structured reason codes for failed checks to power UI messaging and analytics.

Acceptance Criteria
Age calculation by policy
Given a class rule min_age=7y, max_age=9y (inclusive) and age_policy=class_start_date with class_start=2025-09-15 When child DOB=2017-09-16 Then eligibility=false and reason_codes=['AGE_TOO_YOUNG'] Given the same rule When child DOB=2016-09-15 Then eligibility=true and reason_codes=[] Given the same class but age_policy=class_end_date with class_end=2025-12-15 When child DOB=2016-12-16 Then eligibility=false and reason_codes=['AGE_TOO_OLD'] Given age_policy=fixed_cutoff with cutoff_date=2025-09-01 and min_age=7y When child DOB=2018-09-02 Then eligibility=false and reason_codes=['AGE_POLICY_CUTOFF_FAIL'] Given identical inputs across repeated evaluations When eligibility is computed multiple times Then results (eligible flag and reason_codes) are identical
School grade eligibility with term windows
Given grade_min=3 and grade_max=5 (inclusive) and term_window=2025-09-01..2025-12-31 When class_date=2025-10-01 and student_grade=4 Then eligibility=true and reason_codes does not contain 'GRADE_MISMATCH' Given the same rule When class_date=2025-10-01 and student_grade=2 Then eligibility=false and reason_codes=['GRADE_BELOW_MIN'] Given the same configuration When class_date=2026-01-05 and student_grade=2 Then the grade rule is skipped (out of window) and grade-related reason codes are absent and eligibility is determined solely by other active rules
Composite logic and precedence for prerequisites, assessments, and custom flags
Given rule_expression=(Prereq 'Level1' completed AND Prereq 'Level2' completed) OR instructor_approved=true OR has_custom_flag('exception')=true When completions=['Level1','Level2'] and instructor_approved=false and has_custom_flag('exception')=false Then eligibility=true and reason_codes does not contain 'PREREQ_MISSING' Given the same expression When completions=['Level1'] and instructor_approved=false and has_custom_flag('exception')=false Then eligibility=false and reason_codes includes 'PREREQ_MISSING:Level2' Given the same expression When completions=['Level1'] and instructor_approved=true Then eligibility=true and reason_codes=[] Given the same expression AND a NOT has_custom_flag('ban') constraint When has_custom_flag('ban')=true Then eligibility=false and reason_codes starts with 'CUSTOM_FLAG_EXCLUDED:ban' Given the parentheses in the expression When instructor_approved=true and Level2 not completed Then eligibility=true confirming OR precedence after enclosed AND
Reusable rule templates and versioning
Given a published template 'Beginner_Level' v1 with rules [min_age=6, max_age=8, prerequisites=['Intro']] and class A assigned to template v1 with no overrides When the template is updated to v2 [min_age=7, max_age=9] Then class A continues to evaluate with v1 until explicitly migrated and new class assignments default to v2 Given class A migrates to template v2 When eligibility is evaluated Then v2 constraints are applied and the response metadata includes template_id='Beginner_Level' and version='v2' Given class C uses template v2 but overrides max_age=10 at the class level When evaluated Then the override takes precedence and response metadata indicates override_source='class' Given a template is marked deprecated When attempting to assign it to a new class Then the operation is rejected with error code 'TEMPLATE_DEPRECATED'
Structured reason codes for failed checks
Given any ineligibility When evaluation completes Then response includes a reason_codes array of stable identifiers from a controlled set (e.g., AGE_TOO_YOUNG, GRADE_BELOW_MIN, PREREQ_MISSING, INSTRUCTOR_APPROVAL_REQUIRED, CUSTOM_FLAG_EXCLUDED, OUT_OF_TERM) Given multiple failing checks When returned Then reason_codes include all triggered reasons up to a maximum of 5 and are ordered by rule precedence Given a reason code referencing a specific entity When returned Then parameters are included (e.g., {code:'PREREQ_MISSING', params:{prereq:'Level2'}}) and exclude PII Given a successful evaluation When returned Then eligible=true and reason_codes=[] Given localization requirements When returned Then only codes and parameters are emitted and no human-readable messages are included
Consistency across flows and real-time updates
Given child X and class Y When eligibility is requested via search, class detail, cart validation, waitlist, and checkout APIs within the same minute Then all endpoints return the same eligible flag and identical reason_codes Given a rule change (e.g., min_age increased) at t0 When checks are made across endpoints Then updated results are served consistently within 60 seconds and no single request observes mixed states Given an ineligible result at checkout When a payment attempt is made Then the checkout API rejects with 4xx and includes reason_codes; When eligibility later becomes true Then checkout succeeds
Performance and scalability targets
Given a warm system under nominal load of 200 RPS globally When evaluating eligibility for child-class pairs Then p95 latency per evaluation <= 30ms and p99 <= 60ms at the service boundary Given horizontal scaling to multiple instances When load increases 4x Then throughput scales within 20% of linear and latency SLOs are maintained Given bursts of 1000 RPS sustained for 60 seconds When measured Then <0.1% of requests exceed 250ms and error rate <0.1% Given a cold start of a new instance When started Then it becomes ready within 5 seconds and meets latency SLOs after a 30-second warm-up Given a transient datastore outage When evaluation occurs Then the engine serves decisions using cached rule snapshots for up to 5 minutes and includes metadata flag 'STALE_RULES_USED' without accepting bookings that would violate rules once refreshed
Learner Profile & Prerequisite Tracking
"As a parent, I want to add my child’s DOB, grade, and prior achievements once so the system can automatically determine which classes they’re eligible for."
Description

Extends child profiles to capture and validate date of birth, current school grade, and prerequisite evidence (e.g., completed classes, skill evaluations, uploaded certificates). Includes timezone-aware DOB handling, school-year configuration (start month), and grade calculation helpers. Prevents incomplete or inconsistent data entry with field validation and duplicate detection. Stores prerequisite fulfillment with source, date, and verifier to feed the eligibility engine. Ensures data privacy by minimizing exposure of sensitive fields on public surfaces and encrypting PII at rest and in transit.

Acceptance Criteria
DOB Timezone Handling and Age Calculation
- Given a child profile with a specified timezone, When the DOB is saved, Then it is stored as a date-only value (no time/offset) and displays as the same calendar date regardless of viewer timezone. - Given today’s date in the child’s timezone, When age is computed, Then age = floor(years between today and DOB) and does not vary by viewer timezone. - Given a DOB in the future or more than 120 years in the past, When saving, Then the system rejects the value with a clear validation message. - Given a profile without an explicit timezone, When saving, Then the timezone defaults to the organization timezone and is auditable.
School Year Start and Grade Helper
- Given an organization-configured school-year start month (1–12), When viewing/editing a child profile, Then a grade suggestion is computed based on DOB as of the school-year start for the current year. - Given the suggested grade, When the admin confirms it, Then the stored grade remains unchanged until the next school-year start unless an authorized override with reason is provided. - Given an entered grade differing by >1 level from the DOB-based suggestion, When saving, Then the system requires an override reason and logs it. - Given the organization changes the school-year start month, When viewing suggestions thereafter, Then suggested grades recalculate; stored grades do not auto-change.
Prerequisite Evidence Capture and Validation
- Given staff adds a prerequisite fulfillment, When saving, Then the record requires prerequisite ID, source type (completed class/skill evaluation/certificate), source reference (class ID/eval ID/file), verification date, verifier user ID, and status (fulfilled/expired/pending). - Given a certificate upload, When saving, Then only PDF/JPG/PNG up to 10 MB are accepted, files are virus-scanned, checksummed, and stored with a stable reference. - Given verification/expiration dates, When validating, Then verification date cannot be in the future and expiration date (if present) > verification date. - Given user roles, When marking a fulfillment as verified, Then only Instructor/Manager/Admin can verify; others may submit as pending.
Eligibility Engine Data Feed
- Given a child with prerequisite fulfillment records, When the eligibility engine requests data, Then a service returns for that child a normalized list with fields: prerequisite code, status (current/expired/pending), verification date, expiration date, source type, source reference ID, verifier user ID. - Given multiple records for the same prerequisite, When preparing the feed, Then the most recent non-expired record is flagged current; older remain as history. - Given class prerequisites, When evaluating, Then the feed includes eligibility=true if all are current, else eligibility=false with missing_prerequisites[]. - Given concurrent requests, When load tested at 100 RPS, Then p95 response time for the feed is ≤200 ms in staging.
Duplicate Child Detection and Merge Prompt
- Given a user attempts to create a child profile, When name (fuzzy), DOB, and guardian association match an existing child at ≥90% similarity, Then creation is blocked and a potential-duplicate prompt with matches is shown. - Given the duplicate prompt, When the user selects Create anyway, Then a mandatory reason is captured and the new record is flagged for review. - Given the duplicate prompt, When the user selects Merge, Then the merge workflow opens and no additional duplicate record is created. - Given the create form, When running duplicate checks, Then detection completes within p95 ≤150 ms.
Field Validation and Data Integrity
- Given a child profile form, When saving, Then first name, last name, DOB, and at least one guardian link are required; timezone defaults to org timezone if unset. - Given DOB and grade inputs, When validating, Then DOB must be a valid calendar date not in the future and imply an age 0–20 inclusive for child profiles; grade must be within the configured range (e.g., Pre-K–12) or require an override reason. - Given invalid inputs, When attempting save, Then errors are shown inline per field with specific messages and the record is not saved. - Given successful changes, When saving, Then an immutable audit log entry records actor, timestamp, field diffs, and IP address.
Data Privacy and Encryption Controls
- Given PII fields (name, DOB, grade, guardian contacts, certificates), When stored, Then they are encrypted at rest with AES-256-GCM; When transmitted, Then TLS 1.2+ is enforced. - Given public widgets/endpoints, When rendering learner info, Then DOB and grade are never exposed; only non-sensitive indicators (e.g., age band, eligibility) are shown. - Given role-based access, When accessing DOB/grade, Then only authenticated Instructor/Manager/Admin or the child’s guardian may view; all access is logged with purpose-of-use. - Given data export, When exporting by default, Then DOB is excluded; inclusion requires Admin role and explicit confirmation. - Given key management, When operating, Then KMS-managed keys are rotated at least every 365 days and access to keys is logged and restricted.
Class Eligibility Configuration
"As an instructor, I want to configure age ranges, grade requirements, and prerequisites for my class so the right learners can enroll without manual screening."
Description

Provides an admin UI on each class to configure eligibility criteria: min/max age or grade bands, required prerequisites, evaluation/approval requirements, and the policy for age determination (e.g., age on start date). Supports per-class cutoff overrides, season templates, and cloning from existing classes. Validates configurations to prevent contradictory rules and displays the resultant eligibility policy summary. Integrates with scheduling so recurring series inherit policies, with the option to alter by occurrence when needed.

Acceptance Criteria
Configure Age Band with Start-Date Policy
Given I am on the Class > Eligibility tab for a class When I set Min Age to 5 years 0 months and Max Age to 7 years 11 months And I select Age Determination Policy = "Age on class start date" And I click Save Then the configuration saves without error And the policy summary displays "Ages 5y0m–7y11m as of class start date" And reopening the class shows the same values And the class API returns eligibility.age.minMonths = 60, maxMonths = 95, policy = "start_date"
Configure Grade Band with Cutoff Override
Given I am on the Class > Eligibility tab When I enable Grade Eligibility and set Min Grade = 1 and Max Grade = 3 And I set Grade Cutoff Policy to "As of 2025-09-01" And I click Save Then the configuration saves without error And the policy summary displays "Grades 1–3 as of 2025-09-01" And the class API returns eligibility.grade.min = 1, max = 3, cutoffDate = "2025-09-01"
Prerequisites and Approval Requirement Configuration
Given a class has no prerequisites configured When I add prerequisites: "Intro Gymnastics" and "Mat Safety 101" And I toggle "Instructor approval required" on And I click Save Then the configuration saves without error And the policy summary displays "Prerequisites: 2 • Approval required" And the class API returns prerequisites = [ids of Intro Gymnastics, Mat Safety 101] and requiresApproval = true
Validation Blocks Contradictory Rules
Given I am configuring eligibility on a class When I set Min Age to 8 years and Max Age to 6 years And I attempt to Save Then Save is blocked and an inline error appears for Max Age: "Must be greater than or equal to Min Age" And no changes are persisted When I set Min Grade to 4 and Max Grade to 2 And I attempt to Save Then an inline error appears for Max Grade: "Must be greater than or equal to Min Grade" And the Save action remains disabled until errors are resolved
Season Template Save and Apply
Given a class has age, grade, prerequisites, and approval settings configured When I click "Save as Template" and name it "Fall 2025 - Beginner" And I create a new class and apply template "Fall 2025 - Beginner" Then all eligibility fields populate to the template values And the policy summary matches the template summary exactly And the new class API reflects the same eligibility configuration as the template
Clone Eligibility from Existing Class
Given Class A has an eligibility configuration and Class B exists without one When I select "Clone from existing class" on Class B and choose Class A Then Class B’s eligibility settings exactly match Class A’s at the time of cloning And subsequent edits to Class A do not change Class B And a success confirmation displays upon completion
Recurring Series Inheritance and Per-Occurrence Override
Given I create a weekly recurring series with 8 occurrences And I configure eligibility at the series level When I open occurrence #5 and override Min Age to +1 year And I save the occurrence Then only occurrence #5 shows an "Override" indicator and reflects the overridden eligibility And the series-level summary remains unchanged And the series API returns seriesPolicy for all occurrences and a perOccurrencePolicy for occurrence #5
Booking Lifecycle Eligibility Checks
"As an operations manager, I want eligibility enforced and re-checked throughout browsing, checkout, and waitlist promotions so misbookings are prevented end-to-end."
Description

Applies eligibility evaluation consistently at all booking touchpoints: search/browse (filtering and badges), class detail (eligibility status), add-to-cart, waitlist join, payment, and automated waitlist-to-seat promotions. Re-checks eligibility at promotion and payment time to account for birthdays or changed prerequisites. Provides webhook/API hooks so external integrations can request eligibility before creating bookings. Handles edge cases for multi-child carts, mixed eligibility, and time-sensitive holds without degrading performance.

Acceptance Criteria
Search/Browse Eligibility Filtering and Badging
Given a logged-in parent with one or more child profiles containing DOB, grade, and prerequisites When they search or browse classes for a specified date range with at least one child selected Then only classes for which at least one selected child is eligible are shown by default And a "Show Ineligible" toggle reveals ineligible classes And ineligible classes display a badge per child with the primary reason (e.g., "Too young until 2025-11-03", "Grade 3+ required", "Prerequisite: Level 1 not met") And per class, an aggregate indicator shows "X of Y children eligible" And p95 search response time with eligibility enabled is <= 800 ms on a dataset of 5k classes and 50k enrollments
Class Detail Eligibility Status and Move-Up Suggestions
Given a class detail page is opened with a specific child selected (or the first child by default) When eligibility is evaluated for that child Then the page displays status "Eligible" or "Ineligible" with up to 2 concise reasons And if ineligible solely due to age/grade but the child will be eligible by the class start date, show "Eligible on <date>" and enable waitlist join; disable immediate booking And if ineligible due to aging out soon, show a suggestion to book the next level with a link to eligible upcoming sessions And the eligibility status updates within 1s when the user switches the selected child
Add-to-Cart with Multi-Child Mixed Eligibility Handling
Given a parent selects multiple children for a single class When "Add to cart" is clicked Then only eligible children are added as separate cart lines and ineligible children are blocked with inline reasons And a summary shows "Added: N, Blocked: M" with child names And if all selected children are ineligible, no cart line is created and an actionable message is displayed And the cart stores an eligibility snapshot timestamp per line for audit And the operation completes in <= 500 ms p95 for up to 10 children
Waitlist Join with Present/Future Eligibility Rules
Given a class is full and a parent attempts to join the waitlist for a child When eligibility is evaluated Then allow join if the child is eligible now or will become eligible by the class start date And block join if required prerequisites are unmet or eligibility will not be met by start date, showing specific reasons And the waitlist entry stores the eligibility basis (now vs eligibleOn date) for later recheck
Payment-Time Eligibility Recheck and Hold Management
Given a cart with one or more eligible lines under an active hold (e.g., 10 minutes) When the parent proceeds to payment Then the system re-evaluates eligibility for each line item at payment authorization time And if any line has become ineligible due to profile/prerequisite changes, that line is removed with a clear message and is not charged And if all lines are ineligible, block checkout, release the hold immediately, and do not authorize payment And if eligibility has flipped to eligible due to a birthday within the hold window, allow the payment to proceed And eligibility recheck adds <= 150 ms p95 to checkout for up to 10 lines
Automated Waitlist-to-Seat Promotion Recheck and Outcomes
Given a seat becomes available for a waitlisted child When the promotion job/process runs Then eligibility is re-checked at the moment of promotion And if eligible, the child is promoted, a seat hold is applied for the configured window, parent is notified, and if auto-pay is enabled the charge is attempted and confirmed on success And if not eligible, the promotion is skipped, the parent is notified with reasons, and the waitlist position is retained And every promotion attempt is audit-logged with the eligibility decision and reason codes
External Eligibility API/Webhook for Pre-Check
Given an external system needs to validate eligibility before creating a booking When it calls POST /eligibility-check with childId(s), classId(s), and date context Then the API returns 200 with per (child,class) results including eligibility true/false, reasonCodes[], and eligibleOn date when applicable And invalid requests return 4xx with structured error codes; idempotency is supported via a requestId And subscribed partners receive a webhook on eligibility-changed events with before/after snapshots And p95 latency is <= 250 ms for requests with up to 50 classIds; 429 is returned on rate limit with a Retry-After header
Ineligible Visibility & Reason Codes
"As a parent, I want to understand why a class isn’t available and when my child will be eligible so I can plan or choose an alternative."
Description

Controls how ineligible classes appear: fully hidden, shown but disabled, or labeled with clear reason tags (e.g., “Too young until Oct 12” or “Prerequisite not met”). Displays actionable guidance (what’s missing and when eligibility changes) and offers a “Show only eligible” toggle. Computes “eligible in X days” using configured cutoff rules. Ensures messages are accessible, localized, and consistent across web and mobile. Feeds analytics with standardized reason codes to quantify blocked attempts and guide schedule adjustments.

Acceptance Criteria
Eligible Filter Toggle Behavior
Given a signed-in parent is browsing the class catalog with a selected child profile When the user toggles "Show only eligible" ON Then only classes for which the child is eligible are displayed across list, grid, and calendar views within 300 ms of interaction And the results count and pagination reflect the filtered set And ineligible classes do not appear regardless of the configured visibility mode while the toggle is ON When the user toggles the control OFF Then classes are shown according to the configured ineligible visibility mode (Hidden, Shown Disabled, or Shown Labeled) And the toggle state persists for the duration of the session per device and child profile
Ineligible Visibility Modes Configuration
Given the org setting "Ineligible Visibility" is set to Hidden When a child is ineligible for a class Then the class is not rendered in discovery surfaces (search, lists, calendar) and is excluded from public API responses for non-admins Given the org setting is set to Shown Disabled When a child is ineligible for a class Then the class card is visible but the primary action (Book/Enroll) is disabled and cannot proceed to checkout And a "Why not?" affordance or tooltip is available to reveal the reason Given the org setting is set to Shown Labeled When a child is ineligible for a class Then the class card is visible with a prominent ineligibility label and the primary action is blocked with the same label reason And all three modes behave consistently on web and mobile clients
Reason Codes and Guidance Accuracy
Given a class with configured min/max age, grade, and prerequisite flags, and the org's cutoff rule (rolling birthday or fixed school-year cutoff) When evaluating eligibility for a child profile Then the system returns one or more standardized reason codes from the set {AGE_TOO_YOUNG, AGE_TOO_OLD, GRADE_MISMATCH, PREREQ_NOT_MET} And the user-facing label includes specific guidance (e.g., "Too young until {date}", "Prerequisite not met: {name}") mapped from these codes And if the child will become eligible, the message includes "Eligible in X days" and a calendar date computed using the configured cutoff rule and the user's timezone And multiple reasons are displayed in priority order: PREREQ_NOT_MET, AGE_TOO_YOUNG, AGE_TOO_OLD, GRADE_MISMATCH And unit tests around boundary dates (day before/after cutoff and birthday) show a maximum variance of 0 days between expected and displayed dates
Accessibility of Ineligibility Messaging
Given a user relying on assistive technologies When navigating the catalog and encountering ineligible classes Then ineligibility labels meet WCAG 2.1 AA color contrast (≥ 4.5:1) And disabled actions use aria-disabled with an adjacent focusable "Why not?" control that exposes the reason text And screen readers announce the class title, ineligibility status, and reason via aria-live/aria-describedby without redundancy And all interactions are keyboard operable (Tab/Shift+Tab traversal, Space/Enter activation) with visible focus states And color is not the sole means of conveying ineligibility (icon/text present)
Localization and Formatting of Messages
Given the user's locale is set (e.g., en-US, es-ES) When ineligibility messages and dates are rendered Then all strings are sourced from i18n keys and displayed in the selected locale And dates use locale-appropriate formatting and time zones (e.g., Oct 12, 2025 vs 12 oct 2025) And relative text ("in X days") is correctly pluralized and localized And right-to-left locales render labels and icons with correct directionality And missing translations fall back to English while logging a recoverable warning
Analytics for Ineligible Exposure and Blocks
Given analytics is enabled When an ineligible class is displayed to a user Then an exposure event is emitted with fields {org_id, child_id, class_id, reason_codes[], visibility_mode, surface, locale, timestamp} When the user attempts an action that is blocked due to ineligibility (e.g., taps Book) Then a block event is emitted with the same identifiers plus {action, message_key} And events use standardized reason codes, are de-duplicated per session per class, and are delivered within 60 seconds (or queued offline for up to 24 hours) And no PII beyond stable IDs is included And event schemas are documented and versioned
Cross-Platform and API Parity
Given the eligibility evaluation is performed via the Eligibility API When web and mobile clients request class listings for a child profile Then the API returns for each class {is_eligible, reason_codes[], next_eligible_date, visibility_mode} And both clients render identical labels, disable states, and guidance based on this payload And changes to cutoff rules or prerequisites on the server reflect in clients within one refresh cycle And contract tests validate response schema and client rendering parity across supported app versions
Birthday Move-up Suggestions
"As a parent, I want to be notified when my child becomes eligible for the next level so I can easily move them into the right class at the right time."
Description

Detects upcoming birthdays that change eligibility before or within a selected term and proactively suggests move-up classes or levels. Surfaces suggestions in dashboard, class detail, and via email/push with deep links to eligible options. Considers series dates, instructor-defined grace windows, and seat availability to avoid dead-end suggestions. Includes a one-click replace/upgrade from current booking with proration rules respected. Tracks acceptance and conversion for optimization.

Acceptance Criteria
Dashboard Move-up Suggestions for Upcoming Birthdays
Given a student whose birthday occurs before or within the selected term and changes their age-band eligibility for their current program When the eligibility evaluation runs (on enrollment change or scheduled job) Then a move-up suggestion card appears on the studio dashboard within 1 hour including student name, current class, effective date, target level(s), and reason "Age change on <date>" And only target classes with seats ≥ 1 within the relevant dates of the term are included And instructor-defined grace window (± days) is honored when determining whether to suggest a move-up And the card includes actions: View eligible options, One-click replace, Dismiss for 30 days And a dismissed suggestion for the same student-class is suppressed for 30 days unless seat availability changes from 0 to ≥ 1
Class Detail Inline Move-up Recommendation with Deep Links
Given a guardian or staff views the current class detail for a student whose upcoming birthday changes eligibility within the selected term When the page loads Then an inline banner displays a move-up recommendation with the effective date and target level(s) And deep links open a pre-filtered list of eligible, bookable classes (program/level, location, dates) or the specific class when only one option exists And options shown exclude classes without available seats unless a joinable waitlist with auto-convert-to-payment is enabled, in which case options are labeled "Waitlist" And the banner shows an estimated prorated price difference if the replace action is taken And the banner is not shown if no eligible, actionable options exist
Email and Push Move-up Notifications
Given notification preferences permit marketing/operational messages for the guardian And a move-up suggestion becomes available for a student When the suggestion is generated Then an email and a push notification are sent within 2 hours containing student name, effective date, and deep links that open to the eligible options or directly to checkout when only one option exists And no more than 1 move-up message per student is sent within any 14-day period And messages are suppressed if the guardian has dismissed the suggestion within the last 30 days or opted out And links include tracking parameters to attribute engagements and conversions And content is localized to the guardian’s language setting
Eligibility and Grace Window Evaluation
Given an organization term with defined start and end dates and instructor-defined age bands and grace window (± days) When evaluating move-up eligibility for a student with a birthday before or within the term Then eligibility for target classes is determined as of the earlier of the target class start date or the first remaining attended session after the birthday And the grace window is applied to allow early or delayed move-up within the configured bounds And only classes where prerequisites are satisfied and grade constraints are met are considered And suggestions are excluded if the move-up would create schedule conflicts with existing bookings And evaluation is re-run daily and upon enrollment changes
One-Click Replace/Upgrade with Proration
Given a surfaced move-up suggestion and a selected eligible target class When the user clicks One-click Replace Then the system reserves a seat in the target class for at least 5 minutes And calculates prorated charges/credits per organization rules (session-based or date-based), including applicable taxes, fees, and discounts, and displays the net amount due or credit And processes payment via the default payment method; on success, replaces the original booking effective on the move date and issues confirmations to guardian and instructor And on payment failure, releases the reservation, leaves the original booking unchanged, and presents a clear, actionable error And prevents double-booking by enforcing atomicity across cancel-and-book operations
Dead-End Suggestion Filtering
Given evaluation identifies no eligible target classes with seats in configured locations or time windows When preparing surfaces and notifications Then no guardian-facing banner or message is shown And the dashboard shows a muted entry marked "No bookable options" with reason codes (no seats, prerequisites unmet, schedule conflict, outside grace window) And if only waitlist options exist and auto-convert-to-payment is enabled, suggestions may include waitlist options labeled accordingly And suggestions are re-evaluated daily; when a bookable option appears, a new suggestion is surfaced subject to suppression rules
Tracking and Conversion Analytics
Given any interaction with a move-up suggestion When an impression, click, checkout start, replace completion, dismissal, or opt-out occurs Then an event is recorded with suggestionId, studentId, source channel (dashboard, class_detail, email, push), target classId(s), timestamp, and experiment cohort And reporting exposes counts and rates for suggestions generated, surfaced, clicked, started, completed, dismissed, and conversion within a 14-day attribution window And incremental revenue from move-ups (net of proration) is calculated and displayed And events respect privacy/consent settings and exclude opted-out users
Admin Override & Audit Trail
"As a studio admin, I need the ability to override eligibility with justification for edge cases while maintaining a clear audit trail."
Description

Allows authorized staff to bypass eligibility checks for specific children and classes with required reason, scope (single session, series, or program), and expiration date. All overrides are logged with who/when and are visible on the booking and learner profile. Safeguards include optional second-approver workflow and policy alerts when overriding safety-critical prerequisites. Reporting surfaces override rates and reasons to inform rule tuning and training.

Acceptance Criteria
Create Override with Required Fields
Given an authorized staff user with Override:Create permission When they open a child’s profile and select "Create Override" for a target class/series/program Then the form requires selection of scope (Single Session | Series | Program), a specific target, an expiration date, and a reason And the Save action remains disabled until all required fields are valid And expiration cannot be in the past When the user saves a valid override Then the override is created as Active and a success confirmation is displayed And the record stores createdBy user id, createdAt timestamp, child id, target id, scope, expiration, and reason When a user without permission attempts to access "Create Override" Then the action is hidden or disabled and no override is created
Eligibility Bypass Scoped and Expiring Correctly
Given an Active override for Child A with scope=Single Session targeting Session S When attempting to book Session S for Child A Then eligibility checks are bypassed and the booking can proceed And the booking is tagged "Override Used" When attempting to book any other session Then standard eligibility applies Given an Active override with scope=Series targeting Series X When booking any session within Series X for Child A Then eligibility checks are bypassed Given an Active override with scope=Program targeting Program P When booking any class within Program P for Child A Then eligibility checks are bypassed Given an override that has reached its expiration When attempting new bookings for the child Then standard eligibility applies and the expired override is not used
Second Approver Required for Safety-Critical Overrides
Given the setting RequireSecondApprover=true and the target class is flagged Safety-Critical And User U1 submits a new override Then the override status becomes Pending Approval and cannot be used to bypass eligibility And U1 cannot approve their own request Given User U2 with Override:Approve permission When U2 approves the pending override Then the override becomes Active and logs approver id and approvedAt timestamp When U2 declines the pending override Then the override becomes Declined and cannot be used and the decline reason is recorded Given RequireSecondApprover=false or the class is not Safety-Critical When a user creates an override Then it becomes Active without second approval
Policy Alert Prompt on Safety-Critical Override
Given the target class is flagged Safety-Critical When a user initiates an override for that class Then a policy alert modal is displayed containing the safety policy summary And the user must check an acknowledgement box and enter a note before proceeding When the user confirms the policy alert Then the acknowledgement and note are saved with the override record When the user cancels the policy alert Then no override is created and no partial data is saved
Override Visibility on Booking and Learner Profile
Given an Active override applicable to a booking When viewing the staff booking flow Then an "Override Active" badge is displayed with reason (truncated), scope, expiration, and createdBy When viewing the learner profile Then an Overrides section lists Active, Pending, Expired, and Revoked overrides with who/when, scope, target, and a link to audit entries And entries are sorted by createdAt descending And expired overrides are visually distinct and are not applied to future bookings
Comprehensive Audit Trail for Override Lifecycle
Given any override lifecycle event occurs (Create, Edit, Approve, Decline, Revoke, Auto-Expire) Then an immutable audit entry is recorded with event type, actor id, timestamp, child id, target id, and before/after values for changed fields And audit entries are viewable from the learner profile and booking audit panels And audit logs are exportable with applied filters
Override Reporting with Rates and Reasons
Given overrides exist within the selected date range When viewing the Overrides report Then the dashboard displays total overrides, overrides per 100 bookings, percent Safety-Critical, and top reasons with counts And filters for date range, program, series, instructor, and location adjust both counts and rates And a time series chart shows overrides by week within the range And the results can be exported to CSV reflecting the current filters

HealthPass Profiles

Store each child’s allergies, medications, and emergency details in a secure profile that surfaces to instructors on rosters and at check-in. Time-bound documents (doctor’s notes, immunizations, waivers) include expiry reminders and one-tap update requests. Gives staff crucial info when it matters—without repeating forms every session.

Requirements

Secure Child Health Profile Storage
"As a parent/guardian, I want to create and securely store my child’s health profile once so that I don’t have to re-enter it for every class or session."
Description

Implement a unified child health profile that stores allergies, medications (with dosage and administration instructions), conditions, emergency contacts, physician details, and action plans. Data must be encrypted in transit and at rest with field-level protection for highly sensitive items, support version history with change tracking, and persist across all classes/locations under the studio’s brand. Integrate with ClassTap attendee records, support multiple children per account, enable CSV import for legacy data, and define retention/erasure policies to honor parent requests.

Acceptance Criteria
Unified Health Profile Data Model and Validation
Given an authenticated parent or admin creates or edits a child health profile When they submit the profile via UI or API Then the system validates required fields (allergies, medications[name,dosage+unit,administration], conditions, at least one emergency contact[name,relationship,phone], physician[name,phone,practice], action plans[link or file ref]) And rejects invalid input with HTTP 400 and field-specific error codes/messages And accepts valid input and returns HTTP 201 (create) or 200 (update) with child_id and profile_version And enforces formats: phone E.164, emails RFC 5322, ISO 8601 UTC timestamps, dosage numeric with unit from allowed set And enforces limits: text fields <= 2,000 chars, arrays <= 50 items, file size <= 10 MB per attachment And persists the profile under a single child_id keyed to the parent account
Encryption and Field-Level Protection
Given any API request carrying health profile data When the request is made Then transport is enforced via TLS 1.2+ with HSTS and weak ciphers disabled; HTTP is redirected and blocked without TLS Given health data is stored at rest When inspecting storage and backups Then storage volumes are encrypted and sensitive fields (medications, conditions, action plans, physician notes) are field-level encrypted using AES-256-GCM with KMS-managed keys And application logs/analytics contain no plaintext of sensitive fields Given a staff user without Health:ViewSensitive permission When they view a child profile Then sensitive fields are redacted (e.g., “Hidden — insufficient permissions”) And access is denied for decryption calls and is audit-logged with user_id, timestamp, purpose Given a staff user with Health:ViewSensitive permission When they view a child profile for a class they are assigned to Then sensitive fields decrypt server-side and render And all access is audit-logged; repeated access by the same user within 5 minutes is rate-limited to prevent bulk scraping
Version History and Change Tracking
Given a child health profile exists When any editable field is updated Then a new immutable version is created with incremented version number, timestamp, actor_id, and per-field change diffs And previous versions remain viewable by authorized admins with read-only status And restoring a prior version creates a new version preserving the full history (no overwrites) And audit logs capture who changed what and when; edits without authentication or reason are rejected with HTTP 401/403 And the current version id is returned with each read/write response
Brand-Scoped Linking and Attendee Integration
Given a studio brand has multiple locations/classes When a child is enrolled as an attendee in any class under that brand Then the attendee record references the existing child_id (no duplicate profile created) And updates to the profile are reflected across all classes/locations under that brand within 5 seconds And users from other brands cannot access the profile (cross-brand requests return 404/403) And deleting an attendee record does not delete the health profile And the attendee detail API includes a link to fetch the child health profile summary
Multiple Children per Parent Account and Permissions
Given a parent account manages more than one child When the parent adds/edits children Then each child is assigned a distinct child_id linked to the parent account And the parent can view/edit only their linked children; other parents cannot access them And instructors/coordinators can view read-only profiles only for children enrolled in their assigned classes; edits are blocked with HTTP 403 And concurrent edits are controlled with ETag/If-Match; conflicting updates return HTTP 409 with merge guidance
CSV Import for Legacy Health Profiles
Given an admin uploads a CSV for legacy child health profiles When they map columns and validate Then the system shows a preview with row-level validation results and expected operations (create/update/skip) And on import, valid rows are upserted; invalid rows are skipped with a downloadable error CSV including row number and error codes And duplicates are resolved using a composite key (external_id OR name+DOB+parent_email), with deterministic merge rules And the uploaded file is transferred via HTTPS, stored encrypted, and auto-deleted within 24 hours of completion And an import summary is shown/logged: total, created, updated, skipped, duration, initiated_by
Data Retention and Right-to-Erasure
Given a verified parent requests erasure of a child’s health profile When the request is confirmed by the parent and approved by an admin (if required by policy) Then the profile becomes inaccessible within 1 minute and is queued for purge And all primary data is purged within 24 hours; derived indexes/caches within 5 minutes; backups within 30 days And rosters and attendee records immediately stop surfacing health details and show a redacted state And an auditable tombstone (child_id, deletion_timestamp, requestor_id) is retained without health content And a completion confirmation is sent to the parent upon purge finalization
Roster Health Badges & Check-in Alerts
"As an instructor, I want clear health badges and alerts on my roster and at check-in so that I can act quickly on allergies and emergency needs."
Description

Surface key health indicators to instructors within class rosters and the check-in flow via prominent badges (e.g., Severe Allergy, EpiPen, Medication Today). Provide tap-to-expand quick view with critical details, color-coded severity, and acknowledgment prompts. Cache a minimal offline snapshot for on-site access, and auto-sync when connectivity returns. Optionally gate check-in if required health info is missing. Integrate with existing roster UI components and daily class summaries.

Acceptance Criteria
Roster Shows Prominent Health Badges with Severity Colors
Given an instructor views a class roster with students who have health indicators When the roster loads Then each student with indicators displays distinct badges for each active indicator (Severe Allergy, EpiPen, Medication Today, Other) Given severity levels are defined as Critical, Warning, and Info When badges render Then colors map as: Critical=red (Severe Allergy, EpiPen), Warning=amber (Medication Today, General Allergy), Info=blue (Other) Given a roster of up to 50 students on a supported device When the roster data has been fetched Then all health badges render within 1 second Given color may not be perceivable to some users When badges render Then each badge includes an icon and text label and has an accessible name for screen readers Given a student has no health indicators When the roster loads Then no health badge is shown for that student
Tap-to-Expand Health Quick View from Roster and Check-in
Given an instructor taps any health badge on the roster or check-in screen When the action occurs Then a quick view opens within 500 ms showing: student name, condition summary, emergency action steps, medications and dosage/time for today, EpiPen availability/location, emergency contact phone, document expiry statuses, and last updated timestamp Given privacy constraints When the quick view opens Then it does not display full medical history or attachments—only critical summary fields Given the quick view is open When the instructor taps "View Full HealthPass" Then the app navigates to the full profile if the user has permission; otherwise a permission notice is shown Given the quick view is open When the instructor taps outside the panel or selects Close Then the panel dismisses without persisting any changes
Check-in Acknowledgment for Critical Health Conditions
Given a student has a Critical indicator (Severe Allergy, EpiPen, or Medication Today) When the instructor initiates check-in for that student Then an acknowledgment prompt appears listing the critical items Given the acknowledgment prompt is displayed When the instructor taps Acknowledge Then the system records an acknowledgment event with userId, timestamp, classId, studentId, and listed indicators, and proceeds with check-in Given the acknowledgment prompt is displayed When the instructor taps Cancel or dismisses Then check-in does not complete and no acknowledgment is recorded Given an acknowledgment is recorded while offline When connectivity is restored Then the event is synced within 10 seconds retaining the original acknowledgment timestamp Given the class setting "Require acknowledgment for critical indicators" is disabled When initiating check-in Then no acknowledgment prompt is required and check-in proceeds normally
Check-in Gating When Required Health Info Is Missing or Expired
Given the class setting "Gate check-in if required health info missing" is enabled When a student has missing or expired required items (waiver, immunization, doctor's note) at check-in Then check-in is blocked with a clear list of items and a Request Update action Given the blocking dialog is shown When the instructor taps Request Update Then the system sends an update request to the guardian on file and logs the request with timestamp and method Given the blocking dialog is shown and the user's role has override permission When the instructor taps Override and Check In and provides a reason (min 5 characters) Then check-in completes and the override reason is stored with the check-in record Given the class setting is disabled When the student is checked in with missing/expired items Then a non-blocking warning banner is shown, and check-in completes
Offline Minimal Health Snapshot with Auto-Sync on Reconnect
Given the device has no connectivity When the instructor opens the roster or check-in Then a read-only offline snapshot is available per student containing: name, photo (if cached), active badges, critical instruction summary (≤280 chars), emergency contact phone, document status (OK/Missing/Expired), last sync timestamp, and latest acknowledgment status Given offline mode is active When the instructor attempts to edit health data Then the action is prevented with a notice that health data is read-only offline Given offline mode is active When the instructor checks in a student or acknowledges a prompt Then the action is queued locally and marked Pending Sync on the UI Given connectivity is restored When the app detects network Then all queued check-ins and acknowledgments sync within 10 seconds, and snapshots refresh; any Stale banner is removed Given an offline snapshot is older than 24 hours When it is viewed Then a Stale health info banner is displayed
Daily Class Summary Integrates Health Indicators
Given daily class summaries are generated for upcoming classes When a class includes students with health indicators Then the summary includes counts by indicator type (Severe Allergy, EpiPen, Medication Today) and a link to open the roster with the Health filter applied Given summaries are delivered via email or push When the notification is sent Then it contains only counts and class-level flags—no student names or sensitive health details Given the instructor opens the roster from the summary link When the roster loads Then health badges display consistently with the roster view and the filter is applied within 1 second of data fetch
Time-Bound Document Management & Expiry Reminders
"As an admin, I want to track required health documents with expirations so that children cannot attend without up-to-date paperwork."
Description

Enable upload and tracking of required documents (immunizations, waivers, doctor’s notes) with per-document expiry dates and status. Support configurable lead-time notifications (e.g., 30/14/7 days) to parents and admins, bulk reporting for missing/expiring docs, and class-level requirements that block enrollment or check-in when not satisfied. Integrate reminders via email/SMS/push, and maintain versioned document history for audits.

Acceptance Criteria
Upload and Track Time-Bound Documents
Given a guardian or admin uploads a document for a child and selects a document type (immunization, waiver, doctor's note) When a valid file (PDF/JPG/PNG) up to 10 MB and an expiry date are provided Then the document is stored securely, linked to the child, and assigned status "Valid" if expiry date > today in the child's timezone Given an uploaded document with an expiry date When the current date passes the expiry date Then its status auto-updates to "Expired" within 60 minutes and is visible on the child's profile and instructor rosters Given an uploaded document within the admin-defined expiring-soon window (e.g., 30 days) When the profile or roster is viewed Then the status shows "Expiring Soon" with days remaining Given file validation and security requirements When a file type is unsupported or a virus scan fails Then the upload is rejected with an error and no record is created Given a new version is uploaded for the same document type When it is saved Then the latest version becomes Active and previous versions remain in read-only history
Configurable Expiry Reminder Scheduling
Given an admin configures reminder lead times (e.g., 30, 14, 7 days) per document type and delivery window When the settings are saved Then the schedule is persisted and applied to all applicable profiles Given a document is within a configured lead time before expiry When the daily reminder job runs at 09:00 in the child's timezone Then a reminder is queued to the guardian and an alert to admins referencing the specific document and expiry date Given multiple lead times are configured When a reminder has already been sent for a specific lead-time window Then subsequent reminders are suppressed until the next window unless the document is updated Given channel preferences (email/SMS/push) and opt-in status When reminders are sent Then they respect preferences, include a secure expiring link to update, and localize content to the guardian's locale Given a transient delivery failure occurs When sending a reminder Then the system retries up to 3 times with exponential backoff and logs success/failure per channel
Enrollment Gate on Missing/Expired Documents
Given a class has document requirements configured When a guardian attempts to enroll a child lacking required Valid documents for the session dates Then enrollment is blocked with a message listing missing/expired items and provides a direct upload flow Given an admin has override permission When they choose to bypass the block Then they must enter a reason and the enrollment is created with an Overridden flag visible to instructors Given a child is on a waitlist When auto-promotion to enrolled would occur Then promotion is blocked until required documents are Valid and the guardian receives instructions to update Given the guardian completes uploads that satisfy requirements When enrollment is attempted again Then enrollment succeeds without warnings
Real-Time Check-In Gate for Expired Documents
Given an instructor opens a session roster at check-in When the roster loads Then each child displays document status badges (Valid, Expiring Soon, Expired, Missing) Given a child has missing or expired required documents When check-in is attempted Then check-in is blocked and the app offers a one-tap reminder to parents and an admin override requiring signature and reason Given an override is applied When it is saved Then the event is logged with actor, timestamp, and reason, and the child is marked as checked-in with an Override indicator
Missing/Expiring Document Reports
Given an admin opens the Documents Report When filters are set for timeframe (7/14/30/custom days), locations, classes, and document types Then the report lists children by status (Missing, Expired, Expiring Soon) with counts and sortable columns Given the report results are displayed When Export CSV is selected Then a CSV downloads including child, guardian contact, document type, status, expiry date, days to expiry, class/session, last reminder date/time, and channels used Given rows are selected in the report When Send Reminders is clicked Then reminders are sent in bulk respecting rate limits and preferences, and the UI displays per-recipient send outcomes within the report
Versioned Document History and Audit Trail
Given a document is replaced or its metadata changes (expiry date, type) When the update is saved Then a new immutable version is created recording actor, timestamp, IP, checksum, and change summary Given a document has multiple versions When viewed by an authorized user Then they can list versions, preview/download a specific version, and see which version is Active Given an audit export is requested for a child or class When Export Audit is initiated Then a ZIP is generated within 60 seconds for up to 200 documents containing files and a signed audit log (JSON/CSV) of all events Given a data retention policy is configured When a child profile is deleted Then document files are purged and audit metadata is retained with personal identifiers redacted per policy
Class-Level Requirement Configuration
Given an admin configures required document types at the class template or session level When the requirements are saved Then they are inherited by future sessions and can be overridden per session with audit of changes Given requirement rules include validity windows When "Must be valid through session end date" is enabled Then enrollment and check-in gates use that rule to evaluate status Given a public class page is viewed by a guardian When the class has document requirements Then the page displays the required document types without revealing any child-specific data and links to upload after sign-in Given requirements change after enrollments exist When a new required document is added Then impacted enrollments are flagged and guardians are notified to provide the missing document
One-Tap Update Request Workflow
"As a coordinator, I want to send one-tap update requests to parents so that I can collect missing or expired health info without manual follow-ups."
Description

Allow staff to trigger one-tap requests for parents to update profiles or replace expiring documents directly from rosters, profiles, and reports. Deliver magic-link notifications that open a pre-filled, mobile-friendly form supporting camera upload and e-signature. Track request status (sent, viewed, completed), auto-close upon submission, and re-notify on non-response based on configurable cadence.

Acceptance Criteria
One-Tap Request from Roster, Profile, and Reports
Given a staff user with Update Request permission is viewing a class roster When they tap “Request Update” for a child Then a new update request is created with the selected items and a success toast is shown Given a staff user is viewing a child’s HealthPass Profile When they tap “Request Update” Then a new update request is created and linked to that child Given a staff user is viewing the Expiring Documents report When they select one or more children/documents and tap “Request Updates” Then requests are created for each child with the selected items grouped per child Given there is already an open request for the same child and item(s) When a new request is attempted Then the system prevents duplicate open requests and surfaces the existing request with its status
Magic-Link Delivery and Access
Given an update request is created When notifications are sent Then the parent/guardian receives a magic-link via the organization’s configured channel(s) Given a parent opens the magic-link When the link is valid and the request is open Then the update form loads without requiring login and is tied to the correct child/request Given a parent opens the magic-link after the request is completed or expired When the page loads Then the user sees a clear “Request Closed/Expired” message and no editable form Given a magic-link is used multiple times before completion When the link is reopened Then the same open request form loads until completion or expiration
Pre-Filled Mobile Form with Camera Upload and E‑Signature
Given a parent opens the magic-link on a mobile device When the form loads Then existing child data and document metadata are pre-filled and fields needing updates are flagged as required Given a parent needs to replace a time-bound document When they upload via device camera or file picker Then the system accepts common image/PDF formats and enforces file size/type validation with inline error messaging Given sections require consent or acknowledgement When the parent provides an e-signature Then the signature, signer name, and timestamp are captured and stored with the submission Given the form is viewed on a small screen (≤375px width) When the user scrolls and interacts Then all inputs are usable without horizontal scrolling and primary actions remain reachable
Status Tracking and Auto-Close
Given an update request is created When notifications are sent Then the request status is set to Sent with a timestamp Given a parent opens the magic-link When the form renders Then the request status updates to Viewed with a timestamp Given a parent successfully submits the form When the server validates and saves updates Then the request status changes to Completed, the request auto-closes, and completion timestamp is recorded Given a request is Completed When staff view the roster, profile, or report Then the status shown is Completed and the updated data/documents are visible in those views
Reminder Cadence for Non-Response
Given an update request is in Sent or Viewed status and not Completed When the configured reminder interval elapses Then a reminder notification is sent and the reminder count and last reminder timestamp are updated Given a maximum reminder attempt limit is configured When the limit is reached without completion Then no further reminders are sent and the request remains open until completion or manual cancel Given a request becomes Completed at any time When the reminder job runs Then no reminder is sent for that request
Permissions and Audit Logging
Given a staff user lacks Update Request permission When viewing rosters, profiles, or reports Then the “Request Update” action is not visible or is disabled Given a staff user with permission triggers an update request When the request is created Then an audit log entry records the actor, timestamp, child, requested items, and notification channel(s) Given a parent submits updates When the submission is saved Then an audit log entry records submission timestamp, items changed, files received, and signature capture metadata
Batch Requests from Expiring Documents Report
Given a staff user selects up to 500 children/documents in the Expiring Documents report When they send requests in batch Then one request per child is created grouping all selected items for that child and a summary shows counts for created, skipped (duplicate), and failed Given some selected children already have open requests for the same items When the batch is processed Then those are skipped as duplicates and surfaced in the summary with links to the existing requests Given a batch send completes When staff revisit the report Then each child’s row reflects the current request status (Sent, Viewed, Completed) without page refresh
Parent Portal for Profiles & E-Consents
"As a parent, I want a simple portal to manage my children’s health info and consents so that I can keep everything current across all our bookings."
Description

Provide a parent-facing portal to create and manage multiple children’s HealthPass Profiles, upload documents, set medication administration permissions, and e-sign studio waivers and policies. Show real-time status and expiry timelines, reuse profiles across bookings within the studio brand, and let parents manage notification preferences. Ensure accessibility, mobile-first UX, and secure authentication with MFA options.

Acceptance Criteria
Mobile MFA Login and Session Security
Given a registered parent without MFA, when logging in with valid credentials, then they are required to enroll in MFA and can choose SMS, authenticator app (TOTP), or email OTP and generate backup codes before access is granted. Given a registered parent with MFA enrolled, when logging in, then a second factor is required and only a correct code within its validity window grants access; after 6 failed MFA attempts the account is temporarily locked for 10 minutes and an alert email is sent. Given "Remember this device" is enabled, when the parent returns on the same device/browser, then MFA is not prompted for 30 days unless password was changed or risk is detected. Given an active session, when 30 minutes of inactivity elapse, then the session expires and the next sensitive action requires re-authentication with MFA. Given 5 failed password attempts within 15 minutes, when another attempt is made, then login is blocked for 10 minutes and an alert is sent to the account email.
Manage Multiple Child Profiles
Given a parent in the portal, when creating a child profile, then the system requires First Name, Last Name, Date of Birth, and one Emergency Contact (name and E.164 phone); DOB cannot be in the future; all fields validate with inline error messages. Given a child profile is saved, when viewing the child list, then the profile appears with a completeness indicator (Complete/Incomplete) based on required fields and presence of allergies/medications/emergency details. Given an existing child profile, when edits are saved, then changes are audit-logged with timestamp, actor, and before/after values and are immediately reflected in the profile. Given a child has active or future bookings, when the parent attempts to delete the profile, then deletion is blocked with a clear message and the option to Archive; archived profiles are hidden by default and can be restored. Given two sessions edit the same child profile concurrently, when the second save occurs, then a conflict message is shown and the user must refresh to proceed; no silent overwrites occur.
Upload and Track Time-Bound Health Documents
Given a child profile, when uploading a document of type PDF/JPG/PNG up to 10 MB, then the upload shows progress, passes a virus scan, and stores metadata (type, issue date, expiry date, version, optional provider); unsupported types or sizes are rejected with a specific error. Given a required document is stored, when viewing the Documents tab, then each doc displays status (Valid, Expiring Soon <30 days, Expired), its timeline, and a thumbnail/preview. Given a document is within 30, 7, or 1 days of expiry, when notification rules run, then the parent receives reminders per their notification preferences and a one-tap Update CTA opens the prefilled replacement flow. Given a replacement document is uploaded, when it is saved, then the previous version is archived and viewable in history with version numbers and timestamps. Given a required document is expired, when attempting to book a new class, then checkout is blocked until an updated document is uploaded.
E-Sign Waivers and Policies with Audit Trail
Given a studio requires a waiver/policy, when the parent opens the E-Consent, then the current version number and effective date are displayed with mandatory acknowledgment and scroll-to-end before enabling Sign. Given signing is initiated, when the parent completes a signature (typed or drawn) and consents, then the system records signer name, selected child(ren), timestamp (UTC), IP, and user agent and generates a tamper-evident PDF receipt emailed to the parent. Given multiple children, when signing, then the parent can apply the consent to one or multiple selected children and each child’s profile reflects the signed status for the current version. Given a new waiver version is published, when the parent next logs in or attempts booking, then they are required to re-sign the new version; prior signatures remain archived and viewable. Given a required waiver is unsigned for a child, when attempting to finalize a booking, then the flow is blocked with guidance and a direct link to sign.
Set Medication Administration Permissions
Given a child profile, when the parent opens Medication Permissions, then they can add entries with fields: medication name, dosage, form, administration instructions, allowed during sessions (Yes/No), emergency use (Yes/No), consent start/end dates, and notes; required fields validate before save. Given permissions are saved, when viewing the child profile, then each medication displays status (Allowed/Not Allowed/Expired) and last updated timestamp; all changes are audit-logged. Given permissions are Allowed and the child is booked, when the instructor roster is generated, then the medication details and permissions are available to staff in the secure roster/check-in view. Given a consent end date passes, when viewing the profile or roster, then the medication is marked Expired and is not shown as Allowed until renewed; the parent is notified 7 days before expiry per preferences.
Account & Notification Preferences
Given a parent profile, when opening Preferences, then per-channel toggles (Email, SMS, Push) are available for document expiry reminders, update requests, booking confirmations/changes, waitlist-to-payment, and marketing; transactional categories cannot be disabled. Given SMS notifications are enabled, when the parent opts in, then consent is verified via one-time code and recorded with timestamp and source; disabling SMS triggers an opt-out confirmation message. Given preferences are saved, when subsequent events occur, then notifications are delivered or suppressed according to selections; a Send Test option confirms delivery for each enabled channel; changes are audit-logged. Given frequency caps, when multiple reminders would occur the same day, then the system consolidates into a single daily summary per channel without losing critical alerts.
Accessibility and Mobile Experience
Given key portal flows (login, child profiles, document upload, e-sign), when audited with WCAG 2.2 AA, then there are no Critical/Major axe-core violations; all interactive elements are keyboard-accessible with visible focus; all form fields have labels/ARIA; text contrast ratios are ≥ 4.5:1. Given a mid-tier Android device on 4G (e.g., Moto G-series), when loading the portal, then Core Web Vitals meet thresholds: LCP ≤ 2.5s, CLS ≤ 0.1; layouts are responsive for 320–768px without horizontal scroll; tap targets are ≥ 44x44dp. Given VoiceOver or TalkBack is enabled, when navigating forms and modals, then screen reader announcements follow logical order, focus is trapped within modals and returned on close, error messages are programmatically associated, and signature capture has an accessible alternative (e.g., type-to-sign).
Emergency Quick View & Incident Reporting
"As an instructor, I want a quick emergency view and incident report flow so that I can respond and document issues without leaving check-in or class."
Description

Provide a single-tap Emergency Card accessible from rosters and check-in with offline availability, showing top risks, action steps, medication location/dosage, and emergency contacts. Include quick-call buttons and a guided incident report that timestamps events, collects actions taken, and allows sharing with parents and admins. Support PDF export and secure attachment of photos or documents to the incident record.

Acceptance Criteria
Single-Tap Emergency Card Access from Roster and Check-In
Given an instructor is on the class roster or check-in screen When they tap the Emergency icon for a child Then the Emergency Card opens within 2 seconds on 3G+ or 1 second on Wi‑Fi And the card displays: top 3 risks, action steps, medication name/location/dosage, child photo, allergies, age, primary and secondary emergency contacts with labels And quick-call buttons are visible for each contact and local emergency services And tapping Back returns to the prior screen state without losing context
Emergency Card Offline Availability and Staleness Handling
Given the device is offline and the child’s HealthPass profile was synced within the last 24 hours When the instructor taps the Emergency icon Then a cached Emergency Card opens within 2 seconds And a Last Updated timestamp is shown; if older than 24 hours, a staleness warning is displayed And medication info and action steps are accessible offline; online-only attachments are marked Unavailable Offline When connectivity is restored while the card is open Then the card refreshes within 5 seconds and updates the Last Updated timestamp
Quick-Call Buttons Initiate Calls and Handle Permission Errors
Given the Emergency Card is open When the instructor taps a contact quick-call button Then the OS dialer opens with the correct number And if call permissions are denied, an inline error with enable-instructions is shown And if the contact has multiple numbers, a number picker is presented And the call tap is timestamped and recorded against the current incident (if active) or the child’s activity log
Start and Complete Guided Incident Report
Given the Emergency Card is open When the instructor taps Report Incident Then a guided flow opens with steps: Details, Timeline, Actions Taken, Medications Administered, Photos/Documents, Review & Submit And Start Time auto-populates with the current timestamp but is editable; required fields are validated inline And progress auto-saves at each step; the user can Save Draft and resume later with proper access When the user submits Then an immutable incident ID is generated; createdAt and submittedAt timestamps are recorded; a success confirmation is displayed
Attach Photos and Documents Securely to Incident
Given the user is on the Photos/Documents step of the incident report When they capture a photo or select a file (jpg, png, heic, pdf) Then uploads succeed for files up to 25 MB each with a total incident size cap of 100 MB And files are encrypted in transit and at rest; access is limited to staff on the class, admins, and the child’s guardians And on upload failure, retry and remove options are provided; partial uploads do not persist And thumbnails are generated; tapping opens a secure viewer; local caches are purged within 24 hours
Generate and Share Incident Report PDF
Given an incident report is submitted When the user selects Export PDF Then a PDF generates within 10 seconds including child info, incident summary, chronological timeline with timestamps, actions taken, medications administered (dose/time), and attachment thumbnails with filenames And internal-only notes are excluded unless Include internal notes is explicitly selected by an admin When the user selects Share Then authorized recipients (parents, admins) can be chosen; a secure, expiring link (minimum 7 days, configurable) is sent via email; access is logged And if PDF generation fails, a retriable error is shown and no partial PDFs are distributed
Notify Parents/Admins and Track Acknowledgements
Given an incident report is submitted When notifications are sent to selected parents and admins Then recipients receive an email and in-app notification within 2 minutes And parents can view the report after authentication without access to staff-only fields; admins see all fields per their role When a recipient opens the report Then they can acknowledge receipt; the acknowledgement is timestamped and visible to staff And if delivery bounces, staff are alerted with the bounce reason and options to resend or update contact info
Role-Based Access Control & Audit Logging
"As a studio owner, I want strict access controls and audit logs on health data so that we protect privacy and meet compliance obligations."
Description

Define granular access controls so only authorized roles (owner, admin, instructor, front desk, parent) can view or edit specific health fields, with least-privilege defaults. Implement field-level access logs capturing who viewed/changed what and when, plus consent history for document signatures. Provide masked views for lists, session timeouts for sensitive screens, and exportable audit reports to support compliance and parent requests.

Acceptance Criteria
Least-Privilege Defaults Enforced per Role and Field
Given a new tenant with default roles and no custom permissions When each role attempts to view or edit HealthPass fields (allergies, medications, emergency contacts, time-bound documents) Then Owner/Admin can view and edit all fields and documents And Instructor can view allergies and emergency contacts only, cannot view medications dosage/notes, and cannot edit health fields And Front Desk can view allergies and emergency contacts, can mark document receipt status, cannot view medications dosage/notes, and cannot edit health fields And Parent can view and edit only their own child(ren)’s records; access to other children is denied And unauthenticated users cannot access any HealthPass data (HTTP 401) and unauthorized access returns HTTP 403 without field values And API responses exclude fields a role is not authorized to access (no over-fetching) And permission evaluation is enforced server-side and consistent across UI and API
Field View Events Are Logged at Granular Level
Given an authorized user views a child’s HealthPass profile, roster cell, or list item containing health fields When the view occurs via UI or API Then a view log entry is created capturing child_id, field_group/field_names, viewer user_id, role, tenant_id, timestamp (UTC, ms), source (UI/API), request_id/session_id, and client IP/device And masked 'reveal' actions generate distinct view log entries per reveal event And multiple fields loaded in a single render are recorded as one entry per field group (allergies, medications, emergency) to prevent duplication And view logs are read-only, queryable in audit reports, and visible only to Owner/Admin roles
Field Edit Events Capture Full Audit Trail
Given an authorized user creates, updates, or deletes a health field When the change is submitted and persisted Then an audit log entry is recorded with child_id, field_name, action (create/update/delete), previous_value (masked for sensitive fields), new_value (masked for sensitive fields), user_id, role, tenant_id, timestamp (UTC, ms), source (UI/API), request_id/session_id, and client IP/device And edits to sensitive fields (e.g., medications dosage/notes) are masked in reports but stored securely for compliance And audit entries are append-only and cannot be edited or deleted by tenant users And soft-deleted values remain recoverable via audit log
Document Signature Consent History Is Immutable
Given a parent/guardian signs or re-signs a time-bound document (waiver, immunization, doctor’s note) When the signature is captured Then the system records consent with document_id, document_version_hash, signer user_id, signer full name, role, signature type (typed/drawn/uploaded), timestamp (UTC, ms), IP, device fingerprint, locale, and consent text version And each signature event creates a new immutable record; prior records remain unchanged And expiry reminders and one-tap update requests generate log entries including document_id, recipient user_id, channel (email/SMS/in-app), send timestamp, and delivery status And consent and reminder logs are exportable in audit reports with access restricted to Owner/Admin roles
Masked Health Data in Rosters and Lists
Given an instructor or front desk user views rosters or lists containing health indicators When records are displayed Then sensitive values (e.g., medication details) are masked by default and represented by icons or redacted text And a 'Reveal' control is shown only to roles with view permission for that field and is not rendered for unauthorized roles And activating 'Reveal' displays the value for that session only, auto-remasks on navigation or after 60 seconds of inactivity, and creates a view log entry And roster/list exports respect field-level permissions and masking rules
Sensitive Screen Session Timeout and Re-authentication
Given a user is viewing sensitive HealthPass screens (child health details, document viewer, audit logs) When there is inactivity for the configured timeout (default 5 minutes; configurable 1–15 minutes per tenant) Then the screen auto-locks, hides sensitive data, and requires re-authentication (password/SSO; 2FA if enabled) to continue And background tabs are also subject to timeout And lock and unlock events are audit-logged with user_id, timestamp (UTC, ms), and screen context
Exportable, Filterable Audit Reports for Compliance
Given an Owner/Admin requests an audit export When filters (date range, user, role, child, action type [view/edit/sign/reveal/export/login], field name, source) and format (CSV/JSON) are applied Then the system generates an export within 30 seconds for up to 100,000 records, with server-side pagination for larger datasets And timestamps in the export reflect the selected timezone, and all sensitive values are masked per access policy And only Owner/Admin roles can generate exports; each export action is audit-logged and download links expire within 15 minutes And exports include column headers and a checksum or hash of the file contents for integrity verification

SpendGuard Wallet

Shared family wallet with per‑child spending limits, class‑type restrictions, and optional auto‑reload at chosen thresholds. Refunds and credits route back to the wallet and can be earmarked for a specific child. Clear transaction history and alerts help parents stay in control while kids stay consistently enrolled.

Requirements

Household Wallet Setup & Member Linking
"As a parent, I want to create a shared wallet and link my kids’ profiles so that I can centrally fund and control all their class bookings in one place."
Description

Enable a parent (guardian) to create a shared SpendGuard Wallet, add child profiles, and link existing learner accounts under a household with owner and co-manager roles. Support identity and payment method verification, default funding source selection (card/ACH), and secure access controls so only guardians can view and manage wallet settings. Surface wallet balance and quick actions across ClassTap (dashboard, checkout, invoices, class details) and ensure the wallet is selectable as a tender in all booking and payment flows. Preserve historical transactions when adding/removing members and allow transfer of wallet ownership to another guardian. Provide a robust data model for household, members, roles, balances, and earmarked funds to integrate cleanly with existing booking and invoicing services.

Acceptance Criteria
Guardian Creates Wallet with Identity and Payment Method Verification
- Given a logged-in user flagged as a guardian, When they tap Create SpendGuard Wallet, Then the system requires completion of identity verification and verifies at least one funding method (ACH micro-deposit or card authorization) before wallet creation. - Given identity or funding verification fails, When the user retries or exits, Then no wallet record is created, a descriptive error is shown, and an audit entry with failure reason is recorded. - Given both verifications pass, When wallet is created, Then a unique Wallet ID is generated, balance is initialized to 0.00 in account currency, and a default funding source is required and persisted. - Given an ineligible card (expired, blocked) or ACH (unverified), When selected as default, Then the selection is blocked with a validation error and no default is set. - Given wallet creation succeeds, When viewing wallet settings, Then createdAt timestamp, owner user ID, and default funding source are present and immutable except via explicit edit flows with re-authentication.
Guardian Adds Child Profiles and Links Existing Learner Accounts
- Given an existing wallet owner or co-manager, When they add a child, Then required fields (first name, last name, and DOB or grade) must be provided and validated; upon save a Child ID is created and linked to the household. - Given a learner account exists in ClassTap, When the guardian links it by email or learner ID, Then the system matches and links to the child profile after consent check; linkage is visible on the child card. - Given the learner account is already linked to a different household, When linking is attempted, Then the action is blocked with an error explaining existing linkage and offering a request-transfer workflow. - Given duplicate child details are entered matching an existing child in the household, When save is attempted, Then a duplicate warning is shown and no duplicate record is created. - Given a child is linked, When the child has existing bookings/invoices, Then those records remain intact and are attributed to the household wallet where applicable.
Owner and Co-Manager Roles with Guardian-Only Access Controls
- Given a wallet has an owner, When the owner invites a co-manager via email, Then the invitee must verify identity as a guardian before gaining the co-manager role. - Given a co-manager, When they access wallet settings, Then they can view balances, transactions, child profiles, and funding methods, and can perform actions except transferring ownership or deleting the wallet. - Given a child or non-household user, When they attempt to access wallet settings endpoints or pages, Then they receive 403 Forbidden and no sensitive data is rendered. - Given any role change (promote/demote/remove), When the change is confirmed, Then permissions take effect within 30 seconds across web and mobile and are logged with actor, target, timestamp, and reason. - Given 2FA is enabled on the guardian account, When performing sensitive actions (add/remove funding source, change default), Then re-authentication is required and enforced.
Wallet Balance and Quick Actions Visible Across Key Surfaces
- Given a guardian is signed in, When viewing Dashboard, Checkout, Invoice Detail, or Class Details, Then the wallet badge shows current available balance, currency, and status with tolerance of <= 1 second from ledger timestamp. - Given the guardian opens the wallet badge, When selecting Quick Actions, Then options include Add Funds, View Transactions, Manage Funding Sources, and Manage Members, and each deep-links to the corresponding settings page. - Given the user is not a guardian on the household, When viewing the same surfaces, Then the wallet badge and quick actions are hidden. - Given a network error occurs while fetching balance, When the surface renders, Then a fallback “—” plus a retry affordance is shown and no stale balance older than 60 seconds is displayed. - Given a refund/credit posts to the wallet, When the dashboard refreshes, Then the balance reflects the credit within 10 seconds and the transaction appears in history.
Wallet Selectable as Tender in Booking and Payment Flows
- Given a household wallet exists, When a guardian checks out for an eligible class for a linked child, Then SpendGuard Wallet appears as a payment option. - Given wallet balance >= amount due, When the guardian selects Wallet and confirms, Then the amount is deducted atomically from the wallet, a ledger entry is created, and the booking is confirmed in one transaction. - Given wallet balance < amount due and split tender is enabled, When the guardian selects Wallet, Then the UI prompts to use balance plus default funding source for the remainder; upon confirmation both charges succeed or the entire checkout is rolled back. - Given wallet is selected at invoice payment, When payment posts, Then the invoice status updates to Paid and the payment method recorded as Wallet with transaction ID. - Given the class or merchant blocks wallet usage, When reaching tender selection, Then Wallet option is not presented and an info tooltip explains the restriction.
Transfer of Wallet Ownership Between Guardians
- Given a wallet owner wishes to transfer, When they initiate transfer to a verified guardian (existing co-manager or new invite), Then the recipient must accept within 7 days. - Given recipient accepts, When transfer completes, Then owner role moves to recipient, previous owner becomes co-manager, and all wallet data (balance, funding sources, earmarks, transactions, members) remains unchanged. - Given open disputes or chargebacks exist or recipient fails verification, When attempting transfer, Then the transfer is blocked with a specific error and no role change occurs. - Given transfer is pending, When either party views settings, Then a banner shows pending state and effective date; sensitive actions remain restricted to the current owner until completion. - Given transfer completes, When events are emitted, Then notifications are sent to all guardians and an immutable audit record is stored.
Data Model Integrity and Integration with Booking/Invoicing
- Given the household wallet data model, When persisting records, Then entities exist for Household, Members (guardian/child), Roles, Wallet, FundingSource, LedgerEntry, Earmark, and Links to Booking/Invoice with enforced foreign keys. - Given concurrent operations (add child, pay invoice, refund), When ledger writes occur, Then writes are idempotent with requestId, ACID-compliant, and result in a non-negative available balance unless overdraft is explicitly allowed. - Given a member is removed, When querying historical transactions, Then all past LedgerEntry records remain accessible, attributed to the original child/guardian, and excluded only from future eligibility checks. - Given APIs are called by booking and invoicing services, When retrieving wallet balance and earmarks, Then responses meet agreed contracts (e.g., GET /wallets/{id} in <= 300 ms p95, with fields: available, pending, currency, earmarks[]). - Given data export is requested, When exporting wallet ledger, Then a CSV is produced with transactionId, timestamp, type, amount, childId (nullable), bookingId/invoiceId (nullable), and running balance, matching UI totals to the cent.
Per-Child Spend Limits & Class Category Controls
"As a parent, I want to set spending limits and class-type rules for each child so that I can control costs and ensure they only book appropriate classes."
Description

Allow guardians to configure per-child spending caps (weekly, monthly, or custom period) with hard blocks or soft caps requiring approval. Provide class-type/category restrictions based on studio-defined tags (e.g., sport, art, advanced) and age/level flags, enforced during discovery and checkout. Display remaining allowance to the child and at checkout, with clear messaging when a limit or restriction is hit and a path to request guardian approval. Support multi-currency handling consistent with the account’s currency, prorate caps across periods, and include audit trails of limit changes for compliance and support.

Acceptance Criteria
Set and Enforce Per‑Child Spend Caps (Hard vs Soft)
- Given a guardian sets a weekly spend cap of 50 in the account currency for Child A with enforcement=Hard, When Child A attempts purchases that would bring weekly spend above 50, Then checkout is blocked, the wallet is not debited, and a clear message explains the exceeded cap. - Given a guardian sets a monthly spend cap of 120 in the account currency for Child A with enforcement=Soft, When a purchase would exceed the cap, Then the child sees an approval prompt to notify the guardian, the booking is not confirmed until approved, and on approval within 24 hours the checkout completes and allowance is updated; if denied or expired, the checkout is canceled. - Given period boundaries align to the account time zone, When the new period starts, Then the spent amount resets to 0 and the full cap becomes available without manual action. - Given a custom period cap from the 10th to the 25th with value 200, When purchases occur within that window, Then spend is tracked against that cap; outside the window, the cap does not apply. - Given multiple caps exist (e.g., weekly and monthly), When a purchase occurs, Then the system enforces all applicable caps, blocking or routing to approval if any would be exceeded.
Enforce Class Category and Age/Level Restrictions in Discovery and Checkout
- Given a studio tags a class as Advanced and Sport and sets age=12+, When a guardian restricts Child B to categories={Art} and level<=Beginner, Then Child B's discovery results hide the Advanced Sport class and any ineligible classes by default. - Given Child B receives a direct deep link to a restricted class, When attempting checkout, Then the system blocks enrollment, shows the specific restriction reason (category/age/level), and provides a Request Approval option. - Given a guardian whitelists a specific class as an override, When Child B views discovery and checkout, Then that class appears and can be enrolled regardless of category restrictions, while other restrictions remain enforced. - Given age/level flags conflict (e.g., age adequate but level too high), When validating eligibility, Then the UI enumerates all failed checks so the guardian understands why it is blocked.
Display Remaining Allowance and Soft‑Cap Approval Flow
- Given a child is logged in, When viewing their dashboard, Then the remaining allowance amount and period (e.g., "This week") display in the account currency with a progress indicator. - Given the child is at checkout, When the item price plus current period spend would exceed a soft cap, Then the UI displays the overage amount, explains the soft cap, and offers Send Approval Request. - Given the child sends a request, When the guardian approves within the configured window, Then the booking completes automatically, the wallet is debited, notifications are sent to both parties, and audit entries are recorded. - Given the guardian denies or the request expires, When the child returns to checkout, Then the booking remains unconfirmed, the UI shows the denial/expiry, and no funds are held or captured. - Given a hard cap would be exceeded, When at checkout, Then the UI clearly states the hard cap block and does not offer an approval request.
Multi‑Currency Handling for Spend Caps
- Given a guardian creates or edits caps, When viewing cap values and remaining allowance, Then all amounts are defined and displayed in the account currency regardless of studio class currencies. - Given the account currency is USD and a studio charges in EUR, When Child C attempts to book a €40 class with a weekly cap of $50 and $20 already spent, Then the system converts €40 to USD using the current platform FX rate at authorization, verifies cap compliance against USD, and either blocks/approves accordingly with displayed conversion details and rounding. - Given a purchase is approved after a soft-cap request, When capture occurs hours later, Then the same FX rate used at authorization is applied to the deducted allowance to ensure consistency, and the applied rate is shown in the transaction details. - Given fees, taxes, or discounts apply, When computing spend against the cap, Then the net amount paid from the wallet in the account currency is used, excluding non-wallet funding and after discounts. - Given multi-currency rounding rules, When converting, Then rounding follows banker's rounding to 2 decimals in the account currency and never allows exceeding the cap due to rounding.
Prorated Caps and Mid‑Period Changes
- Given a monthly cap of 120 in the account currency is created on the 10th with the account time zone UTC-5, When proration is applied for the remaining days in the month, Then the available allowance equals cap * (remaining days/total days) minus spend to date, floored at zero, and is shown to both guardian and child. - Given a cap value is decreased mid-period below the already-spent amount, When validating remaining allowance, Then remaining equals 0, the system warns the guardian of over-spend, and future purchases are blocked (hard) or require approval (soft) per the cap setting. - Given a cap value is increased mid-period, When recalculating remaining allowance, Then remaining equals new cap minus spend to date without retroactive resets, and the change appears immediately in UI. - Given weekly caps, When period rollover occurs at local week start (Monday 00:00 account time zone), Then remaining resets and an audit entry records the rollover event.
Audit Trail of Limit and Restriction Changes
- Given a guardian creates, updates, or deletes a cap or category restriction for a child, When the change is saved, Then an immutable audit entry records actor (user ID), child, fields changed (before/after), timestamp with time zone, IP/device, and optional reason. - Given an approval request is sent/approved/denied/expired, When status changes, Then corresponding audit events are recorded and linked to the booking attempt. - Given support needs to review history, When querying the audit log by child or date range, Then results can be filtered, exported (CSV), and include a verifiable checksum or sequence number to detect tampering. - Given privacy rules, When displaying audit entries to guardians, Then sensitive data (e.g., IP) is masked while full details remain accessible to authorized support/admin users.
Smart Auto-Reload & Funding Sources
"As a parent, I want the wallet to auto-reload from my saved payment method so that my kids aren’t blocked from enrolling when the balance runs low."
Description

Provide threshold-based auto-reload rules (e.g., top up $50 when balance drops below $20) at the wallet level with optional per-child earmarks. Support multiple funding sources with prioritization and fallback; handle preauthorization, retries, and failure notifications. Include manual top-ups, one-time boosts, and pause/resume controls. Tokenize payment methods for PCI-DSS compliance and store minimal PII. Expose auto-reload status and upcoming reload projections in the wallet UI and alert guardians before and after reload events.

Acceptance Criteria
Wallet-Level Threshold Auto-Reload with Per-Child Earmarks
- Given a wallet rule "Top up $X when balance < $T" is saved, When the wallet balance drops from >= $T to < $T due to a debit, Then an auto-reload for exactly $X is initiated within 60 seconds and a pending transaction is recorded. - Given per-child earmarks totaling $X are configured on the rule, When the reload settles, Then child sub-balances are credited according to the earmarks and the ledger records allocations per child. - Given earmarks that do not sum to $X, When attempting to save the rule, Then the save is blocked and an error explains the mismatch until corrected.
Funding Source Priority and Fallback
- Given at least two funding sources are on file and ordered by priority, When an auto-reload is triggered, Then authorization is attempted on the highest-priority source first. - Given the first source declines or times out, When fallback is applied, Then the next source in priority is attempted within the same reload event before any retries on the first source. - Given all sources are exhausted without approval, Then the reload event is marked failed, no funds are credited, and the failure reason is logged with the attempted sources in order.
Authorization, Retry, and Failure Notifications
- Given an auto-reload is triggered, When contacting the payment gateway, Then a preauthorization for the full amount is created and captured exactly once upon approval using an idempotency key. - Given a network error or soft decline occurs, When retry logic runs, Then up to 3 retries are attempted over the next 24 hours with exponential backoff and idempotency maintained. - Given the final attempt fails, Then the preauthorization (if any) is voided or allowed to expire per gateway rules, the reload is marked failed, and the guardian receives a failure notification within 5 minutes including the amount, source, and decline reason code if available.
Manual Top-Ups and One-Time Boosts
- Given a guardian initiates a manual top-up of $X and selects a funding source, When authorization is approved, Then $X is credited to the wallet immediately and a 'Manual Top-Up' transaction appears in history with source and timestamp. - Given a one-time boost of $Y earmarked to a specific child is submitted, When payment succeeds, Then the child's sub-balance increases by $Y and the ledger entry shows the earmark. - Given the payment is declined, When the top-up or boost attempt completes, Then no funds are credited, the attempt is logged with decline details, and the guardian sees an inline error state.
Pause/Resume Auto-Reload Controls
- Given auto-reload is Active, When the guardian toggles Pause, Then no new reloads are triggered while Paused even if the balance falls below the threshold. - Given auto-reload is Paused, When the balance crosses the threshold, Then a 'reload skipped due to pause' event is logged and visible in history. - Given auto-reload is Paused, When the guardian toggles Resume, Then auto-reload triggering resumes on the next qualifying debit event without requiring rule reconfiguration.
Tokenized Payment Methods and Minimal PII
- Given a new card is added, When the payment method is saved, Then only a network token (or gateway token), brand, last4, and expiry are stored; PAN and CVV are never persisted in databases or logs. - Given a payment is processed, When the gateway request is sent, Then a tokenized identifier is used and CVV is never read from storage (only collected transiently for the request if required). - Given a data export or standard UI view is generated, When inspecting stored payment data, Then no PAN/CVV is present and only minimal PII (e.g., last4, brand, expiry, billing postal code, country) is visible as required for receipts and dispute handling.
UI Status, Projections, and Guardian Alerts
- Given auto-reload is configured, When the guardian opens the wallet UI, Then the page displays: Active/Paused state, threshold ($T), reload amount ($X), funding source priority order, and any per-child earmarks. - Given current balance and known scheduled debits, When projections are computed, Then the UI shows an estimated next reload date/time and amount and refreshes at least hourly. - Given an auto-reload is triggered, When the event starts, Then a pre-reload alert is sent within 60 seconds including amount and funding source; and upon successful capture, a post-reload receipt is sent within 5 minutes.
Refunds-to-Wallet with Child Earmarks
"As a parent, I want refunds and credits to go back into our family wallet and be assigned to the right child so that the money can be reused easily for future classes."
Description

Route refunds and credits from cancellations, no-shows, and studio-issued adjustments back into the SpendGuard Wallet by default, with automatic earmarking to the originating child. Allow guardians to reallocate credits between children, respecting studio policies, expirations, and non-refundable fees. Support partial refunds, split payments, and tax handling, and synchronize credit balances with studio invoices and payout ledgers. Provide clear refund reason codes and event timelines in the wallet ledger to simplify support and reconciliation.

Acceptance Criteria
Default Refund to Wallet on Cancellation (Child Earmark)
Given a confirmed booking B-1001 for Child "A" that is canceled within the studio’s refundable window and a refundable amount of $80.00 is calculated When the refund is processed Then a wallet credit of $80.00 is created and earmarked to Child "A" And the SpendGuard Wallet aggregate balance increases by $80.00 And the wallet ledger records a credit line with reason_code "CANCEL", booking_id "B-1001", child_id for "A", and before/after balance And the ledger entry includes an event_timeline linking "BOOKED" -> "CANCELED" -> "REFUND_POSTED" with UTC timestamps And no funds are returned to external payment methods by default
No-Show With Non-Refundable Fee Withheld
Given a booking B-2002 for Child "B" that totals $60.00, consisting of $50.00 refundable class fee and $10.00 non-refundable studio fee per policy, and the attendance is marked "No Show" When the refund is processed Then a wallet credit of $50.00 is created and earmarked to Child "B" And no credit is issued for the $10.00 non-refundable fee And the wallet ledger records a credit line with reason_code "NO_SHOW" and a note indicating fee_withheld_amount $10.00 with code "FEE_NON_REFUNDABLE" And the aggregate wallet balance increases by $50.00
Partial Refunds from Split Payments Route to Wallet
Given a booking B-3003 for Child "C" totaling $120.00 paid $70.00 from wallet and $50.00 from card, and the studio issues a partial adjustment of $45.00 When the partial refund is processed Then a wallet credit of $45.00 is created and earmarked to Child "C" regardless of the original split funding sources And the wallet and child earmark balances increase by $45.00 And the ledger entry references the original payment sources in metadata and has reason_code "ADJUSTMENT_PARTIAL"
Guardian Reallocation Allowed With Expiration Preserved
Given the wallet holds $60.00 in credits earmarked to Child "A" with expiration 2025-10-01 and the studio policy allows reallocation preserving original expiration When the guardian reallocates $30.00 from Child "A" to Child "B" Then Child "A" earmarked balance decreases by $30.00 and Child "B" earmarked balance increases by $30.00 And both credits retain the expiration date 2025-10-01 And the ledger records two lines: a debit for Child "A" and a credit for Child "B" with reason_code "REALLOCATE" and actor_id of the guardian
Reallocation Blocked by Policy or Non-Transferable Credit
Given the wallet holds $20.00 in credits earmarked to Child "A" flagged non_transferable=true by studio policy When the guardian attempts to reallocate $10.00 to Child "B" Then the system rejects the action with error_code "POLICY_BLOCK" and message explaining the rule And no balances change and no new ledger monetary entries are created
Accurate Tax Handling in Refund Credits
Given a booking B-4004 for Child "D" with subtotal $90.00 and tax $10.00, and the jurisdiction configuration tax_refundable=true When the booking is canceled and the refund is processed Then a wallet credit of $100.00 is created and earmarked to Child "D" And the ledger entry itemizes refund_subtotal $90.00 and refund_tax $10.00 And the studio invoice shows a matching credit memo splitting subtotal and tax
Financial Sync to Invoices and Payout Ledgers Within 60s
Given a wallet credit refund of $75.00 is posted for booking B-5005 and linked to studio S-10 When the refund posts Then a studio credit memo is created for $75.00 with child earmark data, and the payout ledger reflects a $75.00 negative adjustment to revenue within 60 seconds And the sync is idempotent so replays do not duplicate entries And discrepancies trigger a retriable sync job with status surfaced to admins
Unified Ledger & Real-Time Parent Alerts
"As a parent, I want a clear transaction history and timely alerts so that I can monitor spending and act quickly when balances are low or a booking needs approval."
Description

Deliver a searchable, filterable transaction history that shows wallet debits, credits, holds, auto-reloads, approvals, and refunds with per-child tags and class metadata. Include running balances, downloadable CSV export, and reconciliation views for studios. Send real-time alerts via push/email/SMS for low balance, auto-reload events, limit thresholds reached, blocked booking attempts, waitlist promotions, and refunds. Provide per-guardian notification preferences and quiet hours to reduce alert fatigue.

Acceptance Criteria
Unified Ledger: Search, Filter, and Running Balances
Given a parent with a SpendGuard Wallet containing debits, credits, holds, auto‑reloads, approvals, and refunds across multiple children and classes When the parent opens Transaction History and applies filters (date range, child, class, transaction type, amount range, status) Then only matching entries are returned sorted by timestamp desc, with pagination for >500 results, and first page loads in ≤800 ms for ≤500 records And each row displays: timestamp (parent timezone), type, amount, currency, child tag, class title and instructor, reference ID, status, and running balance after the entry And search by free text returns matches on class title, instructor, child name, reference ID within the selected date range in ≤500 ms And running balance equals prior balance ± signed amount for each consecutive entry and never deviates by more than currency rounding rules And hold/capture/release entries are linked via a common hold group ID And refunds appear within 60 seconds of posting and increase balance accordingly
CSV Export: Filtered Transaction History Download
Given any combination of ledger filters is applied When the user clicks Export CSV Then a CSV is generated within 10 seconds containing only the filtered results up to 50,000 rows (with notice to narrow filters if exceeded) And the file name includes parent ID and UTC timestamp And the CSV headers include: transaction_id, timestamp_utc, parent_timezone, type, status, amount, currency, balance_after, child_id, child_name, class_id, class_title, studio_id, reference_id, hold_group_id, source, note And timestamps are ISO‑8601 UTC; numeric amounts use 2‑decimal precision; currency codes are ISO 4217 And the row count and total net amount in the CSV match the on‑screen totals for the same filter set
Studio Reconciliation View: Payout Matching
Given a studio admin opens Reconciliation for a date range When they view the summary Then totals per class and per payout batch show gross, fees, refunds, net, and count of transactions And unmatched ledger entries (not associated to a payout) are highlighted with reason codes And the sum of ledger net amounts for a payout batch equals the payout amount ± ≤$0.01 rounding And the admin can mark items as reconciled with an audit trail (who, when, note) And the reconciliation view and export respect the same filters and produce identical totals
Real-Time Alerts: Delivery, Content, and Dedupe
Given any alertable event occurs (low balance threshold crossed, auto‑reload success/failure, per‑child limit reached, blocked booking attempt, waitlist promotion, refund posted) When the guardian has channel preferences configured Then alerts are sent only via subscribed channels (push/email/SMS) within 5 seconds of event commit And each alert includes: child name (if applicable), event type, amount and currency, remaining wallet balance, class title (if applicable), reason code, and a deep link to the ledger or action And identical events with the same event_id within 60 seconds are de‑duplicated (one alert sent) And if push delivery fails, email is sent within 60 seconds as fallback (if subscribed)
Per-Guardian Notification Preferences and Quiet Hours
Given a guardian configures per‑event channel preferences and quiet hours with a timezone When an alert is generated during quiet hours Then no alert is delivered during the quiet window and the alert is queued And queued alerts are delivered in chronological order within 1 minute after quiet hours end via the selected channels And changes to preferences or quiet hours take effect for new events within 1 minute of saving And queued alerts respect the latest preferences at send time And system time calculations use the guardian’s configured timezone
Blocked Booking Attempt: Alert and Ledger Integrity
Given a child attempts to book a class but is blocked due to class‑type restriction, per‑child spending limit, or insufficient wallet balance When the block occurs Then no debit or hold is written to the ledger and availability is not reduced And an alert is sent to the guardian with the specific reason code and suggested action (e.g., increase limit, add funds) And an auditable non‑financial event is recorded with child, class, reason, and attempt timestamp And repeated identical attempts within 5 minutes send only one alert
Waitlist Promotion: Hold, Approval, and Ledger Entries
Given a child is promoted from waitlist to an available spot When the wallet has sufficient balance Then a hold for the class amount is created with a hold_group_id and an alert is sent to the guardian And if auto‑approval is enabled, the hold is captured immediately and the booking is confirmed; else the guardian has 30 minutes to approve before automatic release And on capture, a debit entry links to the original hold; on release, the hold is reversed and balance restored And if funds are insufficient at promotion, no hold is created, a promotion alert indicates insufficient funds, and the spot is not reserved
Waitlist Promotion Auto-Charge from Wallet
"As a parent, I want promotions from waitlists to be paid automatically from our wallet so that my child doesn’t lose a spot while I’m offline."
Description

Integrate the wallet with ClassTap’s waitlist workflow to automatically secure a seat when a child is promoted, drawing from available wallet funds while honoring per-child limits and earmarks. Place a temporary hold on promotion offers, define acceptance windows, and notify guardians (and optionally the child) with accept/decline actions. On acceptance, convert the hold to a charge; on expiry or decline, release holds and notify the next waitlisted attendee. Include safeguards to prevent overspending and provide clear status updates in the ledger and alerts.

Acceptance Criteria
Hold and notification on waitlist promotion
Given a child is promoted from the waitlist and the wallet has sufficient available funds honoring child earmarks and per-child limits and the class type is allowed When the promotion event is triggered Then create a temporary wallet hold for the total seat cost (tuition, taxes, fees) tied to the child and session And set an acceptance window equal to the configured value And reserve the seat for that child for the duration of the window And send guardians (and optionally the child per settings) a notification with class details, amount, expiration timestamp, and Accept/Decline actions And write a ledger entry "Hold - Waitlist Promotion" with hold ID, child, session ID, amount, and expiry And do not create duplicate holds for the same child and session And if the child has a conflicting booking for the session time, do not create the hold, notify guardians of the conflict, and proceed to the next waitlisted attendee
Accept converts hold to charge and enrolls
Given an active waitlist promotion hold exists for a child and the acceptance window has not expired When a guardian selects Accept Then atomically convert the hold to a settled charge and enroll the child in the class session And deduct funds using the child's earmarked balance first, then the general wallet balance And ensure per-child spending limits are not exceeded And send payment receipt and enrollment confirmation to guardians (and optionally the child per settings) And update the ledger entry to "Charged - Waitlist Promotion" linking to the original hold ID and payment ID And prevent duplicate charges if Accept is submitted multiple times or from multiple channels
Decline or expiry releases hold and notifies next
Given an active waitlist promotion hold exists for a child When a guardian selects Decline or the acceptance window expires Then release the hold and restore reserved funds to their original buckets (earmarked or general) And mark the seat as available to the next waitlisted attendee and trigger their promotion workflow And send a notification confirming the decline or expiry with the final outcome And record "Hold Released - Declined" or "Hold Released - Expired" in the ledger with timestamps and reason And ensure no charges are posted for the declined/expired hold
Insufficient funds and auto-reload handling
Given a child is promoted from the waitlist but available wallet funds respecting earmarks and per-child limits are insufficient And auto-reload is enabled with a valid funding source When the system attempts to place a hold Then initiate auto-reload up to the configured amount to cover the hold And upon successful reload, place the hold, reserve the seat, and send the promotion notification And write a ledger entry for the reload attempt and outcome When auto-reload fails or auto-reload is disabled Then do not create a hold and do not reserve the seat And notify guardians to add funds with a direct link to top up and a clear reason And log the failure reason in the ledger and proceed to notify the next waitlisted attendee
Class-type restrictions and per-child limits enforced
Given a child is promoted to a class session that violates the child's class-type restrictions or would exceed the child's configured spending limit When the promotion event is processed or Accept is attempted Then block the hold/charge and do not reserve the seat And send a notification stating the specific restriction or limit that prevented the promotion And record a ledger entry "Promotion Blocked" with child, session ID, rule violated, and attempted amount And move on to notify the next waitlisted attendee
Ledger, alerts, and idempotency across transitions
Given any transition in the waitlist promotion flow (Hold Created, Accepted, Declined, Expired, Auto-Reload Attempted/Failed, Charge Settled) When the transition occurs Then create a timestamped ledger entry including correlation ID, child, session ID, amounts (hold/charge), prior and new wallet balances, and outcome status And deliver alerts to guardians on their configured channels with clear status and next steps And ensure all operations are idempotent so repeated events or retries do not create duplicate holds, charges, or alerts And expose ledger filters by outcome status and child to support auditability
Funds Hold to Prevent Double-Booking
"As a parent, I want the system to hold funds during checkout so that my child isn’t accidentally double-booked or charged twice for overlapping classes."
Description

When a booking is initiated with wallet funds, place an authorization hold or earmark amount to prevent the same child from being double-booked across overlapping classes or multiple studios. Release the hold on confirmation, cancellation, or timeout according to studio policies. Display hold status and release timestamps in the ledger and ensure holds count toward per-child limits during the pending period to avoid accidental overspend. Provide edge-case handling for concurrent sessions, retries, and network failures.

Acceptance Criteria
Hold Placement on Booking Initiation
Given a parent initiates a booking for a child using SpendGuard Wallet funds and the wallet has sufficient available balance When the parent taps Book/Reserve for a class session Then the system creates an authorization hold (earmark) equal to total due (price + taxes/fees − discounts) for that child And the wallet available balance decreases immediately by the hold amount while total balance remains unchanged And the hold is assigned a hold_id and an expires_at timestamp set per the studio’s hold_timeout policy And the API response includes hold_id, amount, child_id, session_id, status=Held, and expires_at And no additional hold is created for the same request when the same idempotency_key is reused within 2 minutes
Prevent Overlapping Double-Booking Across Studios
Given a child has an active hold or confirmed booking for a time window T1 When a new booking request for the same child targets a session with time window T2 where T1 overlaps T2 (start1 < end2 AND start2 < end1) Then the new booking is rejected before payment capture with error code CHILD_OVERLAP and no new hold is created And this rule applies across all studios and instructors And if the new request references the same session and same booking_id, it is allowed to proceed (idempotent retry) And the rejection response includes conflicting session identifiers and time ranges
Hold Release on Confirmation, Cancellation, or Timeout
Given a hold exists for a booking attempt When the booking is confirmed Then the hold is released and a committed debit is posted for the same amount within 5 seconds, with release_reason=Confirmed When the booking is cancelled by user or studio before confirmation Then the hold is released within 5 seconds with release_reason=Cancelled and no debit is posted When the hold expires at expires_at without confirmation (timeout per studio policy) Then the hold is auto-released within 10 seconds with release_reason=Timeout and no debit is posted And each release updates the wallet available balance and the child’s spend utilization immediately
Hold Counts Toward Per-Child Limits and Wallet Balance
Given a per-child spending limit is configured for the current period When a hold is placed for a child Then the held amount counts toward the child’s spend utilization for the period and toward the wallet’s available balance And any subsequent purchase attempt that would exceed the limit when including active holds is rejected with error code LIMIT_EXCEEDED And if auto-reload is enabled and the available balance falls below the studio-defined threshold due to the hold, a single auto-reload is triggered once per threshold crossing And removing or releasing the hold immediately updates the child’s spend utilization and available balance
Concurrent Booking Sessions and Idempotent Retries
Given two booking requests for the same child and overlapping time windows are submitted concurrently from different devices When both requests reach the server within the race window Then at most one hold is created and the other request is rejected with error code CHILD_OVERLAP without creating a hold And all booking/hold creation APIs require an idempotency_key; if the same key is retried within 2 minutes, the original hold_id and status are returned without creating a duplicate And audit logs record the winning request and the rejected concurrent attempt with timestamps and request_ids
Ledger Visibility of Holds and Releases
Given a parent views the SpendGuard Wallet ledger When there are active or released holds for any child Then each hold appears as a ledger entry with fields: hold_id, child_name, studio_name, class_name, session_time, amount, status (Held/Released), created_at, expires_at, released_at (if applicable), and release_reason And the ledger shows Totals: Available, Held, and Total balance, where Available = Total − sum(active holds) And ledger updates to show a status change from Held to Released within 5 seconds of the event And the ledger is filterable by child and by status (Held/Released)
Network Failure and Stale Hold Auto-Recovery
Given a network error occurs after a hold is created but before the client receives the response When the client retries with the same idempotency_key within 2 minutes Then the server returns the original hold_id and details and does not create a new hold When a hold remains in Held status past expires_at and confirmation was never received Then a background job releases the stale hold within 10 seconds and marks release_reason=Timeout And if releasing a stale hold fails due to downstream errors, the system retries with exponential backoff for up to 15 minutes and raises an alert on persistent failure

Sibling Saver

Flexible sibling discounts that auto‑apply across drop‑ins, packs, and memberships with caps and stacking rules you set. Savings show transparently during checkout and on invoices, reducing support questions and encouraging multi‑child enrollments.

Requirements

Family Linking & Sibling Detection
"As a guardian, I want to link all my children under one family profile so that eligible sibling discounts are recognized automatically across bookings."
Description

Enable guardians to create and manage family profiles that link multiple dependents under a single household while supporting multiple guardians, blended families, and shared custody scenarios. Define sibling eligibility based on shared guardian account/household ID with configurable validation rules and duplicate child detection/merging. Provide secure storage and permission controls so only authorized guardians and studio staff can view and modify family relationships. Expose performant server-side lookups to resolve eligible siblings in real time during cart, waitlist conversion, and invoice generation. Allow admins to manually override or correct family links with an auditable trail.

Acceptance Criteria
Guardian Creates and Manages a Multi‑Guardian Family Profile
Given an authenticated guardian with a verified email When they create a family profile and add at least one dependent with all required fields Then the system creates a unique household ID and links the dependent to that household And an invitation can be sent to additional guardians by email, expiring after 7 days And upon acceptance, the invited guardian is linked to the household with role-based permissions (Owner or Guardian) And guardianship type (primary, secondary, shared) can be set per dependent and is stored with effective start/end dates And validation prevents creation when required fields are missing or the guardian already owns a household (unless joining by invite)
Sibling Eligibility Resolution for Discount Determination
Given a cart containing enrollments for multiple dependents associated to one or more guardians And studio-level eligibility rules are configured (require shared household: true/false, require verified guardian: true/false) When the server-side sibling lookup is invoked for the cart Then it returns a deterministic set of eligible sibling groups based on the configured rules and active guardianship on the purchase date And dependents linked only by inactive or unverified guardians are excluded when the rule requires verification And the response includes child IDs per group and exclusion reasons per non-eligible dependent And performance meets P95 ≤ 200 ms and P99 ≤ 500 ms for the lookup under 100 requests/second per studio And results are consistent across repeated calls within the same transaction (idempotent)
Real‑Time Sibling Lookup During Cart, Waitlist Conversion, and Invoice Generation
Given the system is calculating prices during cart review, waitlist-to-payment conversion, or invoice generation When any of these events occur Then the backend performs a single sibling eligibility lookup per transaction and caches it for the transaction lifetime And discounts are applied consistently across drop‑ins, packs, and memberships according to eligibility results And concurrency controls prevent double-application or omission of discounts during simultaneous conversions And the added end-to-end latency from lookup is ≤ 50 ms median and ≤ 200 ms P95 per transaction And if the lookup fails, the system logs the error, returns a clear non-blocking message, completes checkout without sibling discounts, and flags the invoice for review And retrying the operation yields the same outcome (idempotent)
Duplicate Dependent Detection and Merge Workflow
Given a guardian or staff attempts to add a dependent When the system evaluates potential duplicates using name, date of birth, and household/guardian signals Then candidates scoring ≥ 0.90 confidence are blocked from creation and a merge option is offered to authorized users And merges preserve the canonical dependent ID, consolidate enrollments, waitlists, balances, waivers/consents, and emergency contacts with defined field precedence And an immutable audit record captures actor, timestamp, prior values, new values, and reason code And candidates scoring 0.70–0.89 are placed into a manual review queue; < 0.70 proceed as new And cross‑studio records are never auto‑merged; only platform admins may merge them explicitly
Admin Manual Override of Family Links with Audit and Rollback
Given a studio admin with edit-family permission When they link or unlink guardians and dependents, change household membership, or override sibling eligibility on a specific invoice Then the system requires a reason code and records an immutable audit entry including actor, timestamp, IP, fields changed, and before/after values And changes apply to future calculations only; paid invoices are not altered retroactively—adjustments post as credits/debits when needed And a rollback action restores the prior state when possible and appends a new audit entry And webhooks/events are emitted for downstream systems upon change and rollback And permission checks prevent non-admins from performing overrides; platform admins can perform cross‑studio changes
Role‑Based Access Control and Data Privacy for Family Relationships
Given users with roles (Guardian, Invited Guardian, Studio Staff, Platform Admin) When accessing family profiles and relationships Then guardians can view/edit only their own household and dependents they have rights to; invited guardians have no access until acceptance And studio staff can view relationships only for dependents enrolled at their studio; edits require explicit edit-family permission And cross‑studio data isolation prevents viewing unrelated households or dependents And all access attempts (success and failure) are logged with actor, timestamp, and purpose; unauthorized requests return 403 without revealing record existence And PII fields for dependents and guardians are encrypted at rest, redacted in logs, and never exposed in client-side tokens
Sibling Discount Rule Builder
"As a studio owner, I want to configure tiered sibling discounts with caps and exclusions so that my pricing stays competitive without eroding margins."
Description

Provide an admin interface to create flexible sibling discount rules that support percentage or fixed-amount discounts across drop-ins, class packs, and memberships. Allow targeting by product, class, instructor, schedule, location, and membership tier with effective dates, time zones, and blackout periods. Support tiered discounts by child count (e.g., 2nd child 10%, 3rd child 15%) and define caps per line item, per order, and per family per billing period. Define stacking precedence with other promos and gift credits, conflict-resolution policies, and exclusions (e.g., already-discounted items). Include rule versioning, draft/publish, and rollback for safe changes.

Acceptance Criteria
Create Percentage and Fixed-Amount Sibling Discounts Across Product Types
Given I am an admin with Discount Manager permissions, When I create a rule with Discount Type = Percentage and set 10%, Then preview calculations show 10% off each eligible drop-in, pack, and membership line item. Given I select Discount Type = Fixed Amount and set 5 in the account currency, When previewing an eligible cart, Then a 5.00 currency-unit discount is applied per eligible line item subject to caps. Given all required fields (rule name, discount type and value, targets, at least one child tier, at least one cap, effective dates, time zone) are valid, When I click Save, Then the rule is saved as Draft with version = 1 and appears in the rule list. Given any required field is missing or invalid (e.g., negative value, overlapping blackout dates), When I attempt to save, Then inline validation errors are displayed and the rule is not saved. Given I open rule preview, When I input a sample cart with multiple product types, Then the preview returns per-line discount amounts and a total discount matching the configured type and value.
Target Discounts by Product, Instructor, Schedule, Location, and Membership Tier
Given a rule targets Products {Yoga 101, Ballet Pack}, Instructor = "Lee", Location = "Studio A", Schedule window = Sat 09:00–12:00, Time Zone = America/New_York, Membership Tier = Gold, When a cart line item matches all selected attributes, Then the discount is eligible for that line item. Given any selected targeting attribute does not match for a line item, When evaluating eligibility, Then the discount does not apply to that line item. Given a targeting attribute is left unselected, When evaluating eligibility, Then that attribute imposes no constraint (treated as ANY). Given a schedule window and a rule time zone are configured, When evaluating class times, Then class start times are interpreted in the rule’s configured time zone. Given multiple eligible and ineligible items are in the same order, When eligibility is evaluated, Then only the matching items receive the discount and the non-matching items do not.
Apply Tiered Discounts by Child Count
Given a family account with three children in the same order and a rule with tiers {2nd child: 10%, 3rd child: 15%, 4+ child: 20%}, When checkout occurs, Then the oldest-priced eligible child line remains full price, the 2nd child lines receive 10% off, and the 3rd child lines receive 15% off. Given only one child is present in the order, When evaluating tiers, Then no sibling discount is applied. Given a child is removed or added to the order, When recalculating, Then tier assignments update immediately and resulting discount amounts reflect the new child count. Given multiple eligible line items exist per child, When applying tiers, Then the tier rate applies consistently to all that child’s eligible line items in the order. Given children are linked to the same family account, When evaluating tiers, Then cross-customer linking is not considered (only within the same family).
Enforce Discount Caps at Line Item, Order, and Family Billing Period Levels
Given a rule with a per-line cap of 10.00 currency units, When a percentage discount exceeds 10.00 for a line item, Then the applied discount for that line is limited to 10.00. Given a rule with an order-level cap of 25.00, When the sum of sibling discounts across the order would exceed 25.00, Then discounts are proration-limited in evaluation order until the 25.00 cap is reached and no further sibling discount is applied in that order. Given a rule with a family monthly cap of 100.00 and Time Zone = America/Los_Angeles, When cumulative sibling discounts for the family in that calendar month (per the configured time zone) reach 100.00, Then subsequent orders in the same month receive 0.00 sibling discount and a reason code indicates "Family cap reached". Given multiple caps are configured, When applying discounts, Then the system enforces caps in the order: per-line cap, then order cap, then family-per-period cap. Given discounts are calculated, When totals are displayed, Then amounts are rounded using standard currency rounding to 2 decimals and totals reconcile across line, order, and family levels.
Stacking Precedence, Exclusions, and Conflict Resolution
Given stacking precedence is configured as Sibling Saver before Promo Codes and after Gift Credits, When all three could apply, Then the engine applies Gift Credits first, then Sibling Saver, then Promo Codes to the remaining balance. Given "Exclude already-discounted items" is enabled, When a line item already has a manual or promo discount, Then the sibling discount does not apply to that line and a reason code is recorded. Given exclusions are configured for specific products/categories, When evaluating eligibility, Then excluded items never receive the sibling discount. Given two sibling rules match the same line and conflict-resolution policy = "Highest priority wins" with Rule A priority 1 and Rule B priority 2 (lower number = higher priority), When calculating, Then only Rule A applies. Given conflict-resolution policy = "Greatest benefit wins" and two rules of equal priority match, When calculating, Then the rule yielding the larger discount amount is applied and the other is suppressed with a reason code. Given stacking is disabled with promo codes, When a promo code is present, Then the sibling discount is suppressed according to the policy and the UI displays which discount took precedence.
Rule Versioning, Draft/Publish, Scheduled Effective Dates, Blackouts, and Rollback
Given a Published rule v1 exists, When I edit and save changes, Then a Draft v2 is created without affecting v1. Given Draft v2 has Effective Start = 2025-10-01 00:00 in Europe/London, When I publish v2, Then v2 is scheduled to become active at that instant in Europe/London and v1 remains active until then. Given Blackout Periods are set for 2025-12-20 to 2025-12-26, When a purchase occurs in that window, Then the rule does not apply and a reason code indicates blackout. Given I initiate a rollback from v2 to v1, When confirmed, Then v1 becomes the active version immediately and v2 is archived; the audit log records user, timestamp, and reason. Given a rule is published with Effective Start = Now, When saved, Then the new version takes effect immediately and is reflected in live eligibility and preview endpoints within 60 seconds.
Transparent Savings Display During Checkout and Invoices
Given eligible items in the cart, When sibling discounts are applied, Then checkout displays a per-line "Sibling Saver" row showing child ordinal (e.g., 2nd child), discount amount, applied rule name, and version. Given a cap limits the discount, When rendering the discount line, Then the UI shows a note "Cap reached" with applied vs potential discount amounts. Given an item is excluded or a conflict suppresses a rule, When rendering checkout, Then a non-blocking info tooltip shows the suppression reason (e.g., excluded item, higher-priority rule applied). Given the order is completed, When the invoice is generated (web, PDF, email), Then the same discount details appear per line, and the invoice total discount equals the sum of applied sibling discounts shown at checkout. Given admin views the order in the back office, When inspecting discount details, Then the system displays the evaluation trace: matched rule, stacking sequence, caps applied, and reason codes.
Auto-Apply & Stacking Engine
"As a purchaser, I want sibling savings to be applied automatically at checkout so that I don’t have to enter codes or contact support."
Description

Implement a server-side engine that evaluates cart contents and family context to automatically apply the optimal sibling discounts at checkout, on invoices, and during waitlist-to-payment conversions. Enforce deterministic stacking rules with other discounts, respect configured caps and eligibility, and prevent double-discounting across product types. Ensure idempotent calculations for retries and asynchronous flows, with clear error handling and fallbacks to manual adjustments by admins. Cache intermediate computations for performance while maintaining accuracy with real-time inventory and pricing changes.

Acceptance Criteria
Auto-Apply Optimal Sibling Discount at Checkout
Given a family account with two or more eligible children and a cart containing eligible line items (drop-ins, packs, memberships) And sibling discount tiers and product eligibility rules are configured When the user views Order Summary at checkout Then the engine automatically applies the combination of sibling discounts that yields the lowest payable total without violating any rules And each discounted line item displays a "Sibling Saver" label with the child index and discount percentage And the order-level discount total equals the sum of line-item sibling discounts within ±$0.01 rounding tolerance And the generated invoice mirrors the checkout discount breakdown and totals And if fewer than two eligible children are present, no sibling discount is applied
Caps and Eligibility Enforcement Across Product Types
Given caps are configured (per-child, per-order, and per-family monthly) and certain SKUs/product types are excluded When the engine calculates sibling discounts for a cart with mixed eligible and ineligible items Then discounts never exceed any configured cap; if a cap would be exceeded, the sibling discount is reduced to the remaining allowable amount And excluded items receive no sibling discount and display a reason (e.g., "excluded product type") And tiered sibling percentages are applied by child index consistently across eligible product types And the final payable total reflects capped adjustments exactly, with no negative line-item subtotals
Deterministic Stacking and Double-Discount Prevention
Given stacking rules are configured (stackable true/false, application order, and conflict resolution strategy) And another discount (e.g., promo code) is applicable to one or more line items And some items may be pre-discounted via pack credits or membership-included pricing (non-discountable) When discounts are calculated Then if stackable=false, only the single discount that produces the lowest net price is applied per line item deterministically (tie-breaker: rule priority then createdAt) And if stackable=true, discounts are applied in the configured order, with each subsequent discount applied to the prior net price And sibling discounts are never applied to non-discountable amounts or already zeroed portions And a line item cannot receive more than one sibling discount And the discount breakdown shows the stacking order and reasons when a discount is skipped
Idempotent Calculations on Payment Retries and Webhooks
Given an order with a stable input hash derived from family context, cart contents, pricing snapshot, and rules version When the discount calculation is invoked multiple times for the same order due to UI refreshes, server retries, or webhook redeliveries Then the engine returns identical discount IDs, amounts, and breakdown for the same inputs And no duplicate discount rows or adjustments are created on the order or invoice And concurrent invocations use an idempotency key to ensure a single committed result
Waitlist-to-Payment Conversion Consistency
Given a waitlisted enrollment converts to payment for a family with multiple eligible children And the offer retains the same items and prices as presented to the user When the engine applies discounts during conversion Then the sibling discounts match what would have been applied at checkout for the same inputs And the resulting invoice and receipt show the same discount labels and amounts as the order breakdown And if rules/pricing snapshots were captured for the offer, those snapshots are used; otherwise the current rules version used is recorded in the breakdown
Cache Performance and Invalidation with Real-Time Changes
Given caching of intermediate discount computations is enabled with a configurable TTL When the same cart and family context are recalculated within the TTL Then the response is served from cache and meets configured latency SLOs (e.g., p50 and p95 thresholds) And when inventory, base price, or discount rules/caps change, the relevant cache entries are invalidated before the next calculation And subsequent calculations reflect the updated data with no stale totals returned
Admin Fallback and Error Handling
Given a discount calculation error occurs (e.g., rules evaluation exception or timeout) When the engine cannot complete within the configured timeout Then checkout proceeds without sibling discounts applied and surfaces a non-blocking message to the user (no sensitive details) And a structured error is logged with correlation ID and context, and the order is flagged for manual adjustment by an admin And an admin can add a manual discount adjustment on the invoice that is clearly labeled and reflected in the customer-facing invoice
Transparent Savings Display & Invoicing
"As a parent, I want to see exactly how sibling discounts were calculated on each child’s booking so that I can trust the pricing and keep records for my family budget."
Description

Show clear, itemized sibling savings throughout the customer journey including cart, checkout, confirmation, receipts, and invoices. Display per-child labels, original price vs discounted price, applied rule names, and remaining cap usage to reduce confusion and support questions. Provide accessible tooltips and help text explaining how savings were calculated, and ensure consistent formatting across web, mobile, email, and downloadable invoices. Include an admin view that mirrors the customer breakdown for quick support resolution and adjustments.

Acceptance Criteria
Cart & Checkout: Itemized Sibling Savings for Multi‑Child, Stacked Rules
Given a cart with at least two children enrolled across eligible products (drop‑ins, packs, memberships) And sibling discount rules with stacking and a family cap are configured and applicable When the user views the Cart and Checkout Summary Then each line item displays: per‑child label, original price, itemized discount amount(s) with applied rule name(s) in order, discounted price, and remaining cap usage (e.g., “Remaining this period: $X of $Y” or “Cap reached”) And the Total Savings equals the sum of all discount amounts across line items And Subtotal, Taxes, Total Savings, and Total Due are shown and correctly computed to two decimal places And changing quantities or removing a child updates itemized discounts, totals, and remaining cap display within 2 seconds of the update
Post‑Purchase: Confirmation, Email Receipt, and PDF Invoice Consistency
Given an order is completed that includes sibling savings When the customer views the confirmation page, the emailed receipt, and downloads the invoice PDF Then all three surfaces contain an identical itemized savings breakdown per line: per‑child label, original price, discount amount(s) with applied rule name(s), discounted price, and the cap usage at time of purchase And Subtotal, Taxes, Total Savings, and Total Due match numerically across all three surfaces And each surface contains a Savings section header and the text labels for each applied rule name And the invoice/receipt shows invoice number, invoice date/time in the account’s timezone, and customer name And the PDF renders all savings lines without truncation or clipping and preserves currency formatting
Accessibility: Tooltips and Help Text for Savings Calculation
Given an info control labeled “Savings details” is present next to each discounted line and the order total savings When the user hovers the control with a mouse, focuses it via keyboard, or taps it on touch devices Then a tooltip or modal opens showing: applied rule name(s) in order, per‑line calculation (Original − Discount(s) = Discounted), and cap usage (used and remaining values) And the control is keyboard focusable, opens with Enter/Space, closes with Escape or outside click/tap, and returns focus to the trigger on close And the control has an accessible name/aria‑label, the tooltip/modal content is announced by screen readers, and a visible focus indicator is present And color contrast for text and icons in the tooltip/modal is at least 4.5:1, and content remains within the viewport on small screens with scrollable content as needed
Admin Console: Mirrored Savings Breakdown for Support
Given an admin opens an order or invoice in the Admin Console When the admin views the Sibling Savings section or invoice preview Then the display mirrors the customer view: per‑child labels, original price, itemized discount amount(s) with applied rule name(s) and order, discounted price, and cap usage at time of purchase And an indicator shows the cap remaining after this purchase And the “View customer invoice” preview and downloadable invoice contain the same itemization and totals as the customer’s copy And if a post‑purchase admin adjustment is applied, it appears as a separate labeled line item without modifying the original sibling savings entries
Formatting & Localization: Consistent Currency, Labels, and Layout
Given the account’s currency and locale settings are configured When savings are displayed on web, mobile, email, and PDF Then currency symbols, thousand separators, and decimal precision (two decimals) are consistent across all surfaces And per‑child label formatting is consistent across all surfaces And dates/times on confirmation pages, receipts, and invoices use the account’s timezone And on screens ≤ 375px width, all savings fields (original price, discount amount(s) with rule name(s), discounted price, and cap usage) are visible without truncation; secondary details may be behind an explicit expand control And email and PDF layouts render all savings lines without overflow, clipping, or orphaned labels across page breaks
Cap Tracking: Remaining Usage and Cap‑Reached Messaging
Given a family sibling savings cap is configured with existing prior usage When the cart/checkout applies discounts Then the UI displays the remaining cap before purchase with correct values based on prior usage and current discounts And after order completion, the confirmation and invoice show the updated cap remaining after this purchase And when the remaining cap is zero, no sibling savings are applied, and the UI displays a clear “Cap reached” message with a link to learn more And when a new cap period begins, the next cart correctly reflects the reset cap and removes the “Cap reached” state
Membership Proration & Recurring Billing Discounts
"As a studio owner, I want sibling discounts to work correctly on recurring memberships so that families get fair pricing without breaking my billing and accounting."
Description

Apply sibling discounts to memberships with accurate first-cycle proration, future-dated starts, and mid-cycle joins, ensuring recurring invoices respect per-family monthly caps. Recalculate discounts on upgrades, downgrades, pauses, and cancellations with appropriate credits and refunds. Ensure compatible tax, fee, and surcharge calculations post-discount and produce balanced ledger entries for accounting and revenue recognition. Support multi-location and multi-currency scenarios consistent with existing billing infrastructure.

Acceptance Criteria
Mid-Cycle Join Proration: Sibling Discount With Monthly Cap
Given a family where Child A is already on a monthly membership (no sibling discount) and Child B joins the same plan on day 16 of a 30-day cycle priced at 100.00 in the plan currency, with a sibling discount rate of 20% and a monthly family discount cap of 30.00 When the first prorated invoice for Child B is generated Then the prorated base price is 100.00 * 15/30 = 50.00 And the calculated sibling discount is 50.00 * 0.20 = 10.00 And the applied discount equals min(10.00, remaining monthly cap) and the remaining cap is decremented by the applied discount And all amounts round to 2 decimals using round half up And the invoice contains a separate "Sibling Saver" line item for the discount and displays the remaining cap And the ledger journal for the invoice balances (total debits equal total credits) and records the discount to a contra-revenue account
Future-Dated Start: Discount Applied at Activation With Cap Check
Given Child B purchases a membership on the 20th with a start date of the 1st of next month, discount rate 20%, and a monthly family discount cap of 30.00 And the family's billing time zone is configured When the first invoice is generated at 00:00 on the start date in the family's billing time zone Then the discount applied equals min(plan price * 0.20, remaining cap at invoice generation) And no payment is captured before the start date And if the remaining cap is 0.00, no discount is applied and messaging indicates the cap was reached And checkout shows an estimated discount with a note that the final amount will be recalculated at activation
Recurring Billing: Per-Family Monthly Cap Enforcement and Allocation Order
Given a family with multiple active memberships eligible for a 20% sibling discount and a monthly family discount cap of 30.00 And the family's billing time zone is America/New_York When recurring invoices are generated throughout the month Then the sum of discounts applied to all family transactions in the same calendar month (in the family's billing time zone) never exceeds 30.00 And discount allocation is deterministic: charges are processed in ascending created_at timestamp order; if a discount would exceed the remaining cap, only the remaining cap is applied to that charge and subsequent charges receive 0.00 discount And the cap resets at 00:00 on the first day of each month in the family's billing time zone And invoice and admin views show cap used and cap remaining for the month
Mid-Cycle Upgrade/Downgrade/Pause/Cancel: Recalculated Discounts, Credits, and Refunds
Given an active discounted membership within a family cap When the plan is upgraded or downgraded mid-cycle Then proration charges/credits are computed on the net-after-discount amounts using the discount available at the time of change and the family cap is adjusted by any additional discount applied or restored by any reversed discount And the invoice(s) and ledger reflect balanced proration entries and updated discount line items When the membership is paused effective a future date Then discounts stop accruing from the pause effective date forward and any previously-applied discount for the paused period is reversed and returned to the monthly cap When the membership is canceled mid-cycle with a refund Then the refund amount is based on the net-after-discount value of the unused period, does not exceed collected funds, and reverses the corresponding discount and cap consumption; the ledger records refund, tax reversal (if applicable), and discount reversal with balanced entries
Tax, Fee, and Surcharge Calculation Order After Discount
Given a taxable membership priced at 100.00 with a 20% sibling discount and a 10% tax rate When an invoice is generated Then the taxable base is computed as 100.00 - 20.00 = 80.00 and tax is 80.00 * 10% = 8.00 and the invoice total is 88.00 (excluding any fees/surcharges) And for tax-inclusive pricing, the discount reduces the tax component proportionally and reported tax reflects the reduced amount And configured surcharges/fees are applied per configuration relative to discount and tax (e.g., percentage processing fees applied after discount and before or after tax as configured) and appear as separate lines And invoice and tax reports show base, discount, tax, and fees by line with correct jurisdiction codes
Multi-Location and Multi-Currency: Discount, Cap, and Rounding Rules
Given a family with billing currency USD and two memberships: Child A at Location US (USD 100.00) and Child B at Location CA (CAD 120.00), sibling discount 20%, monthly family cap USD 30.00, and a configured FX rate CAD->USD of 0.75 at charge time When both invoices are generated Then discounts are applied as 20.00 USD on Child A and 24.00 CAD on Child B And cap consumption is tracked in USD; the CAD discount is converted at 0.75 to 18.00 USD for cap purposes; total cap used is 38.00 USD but the applied discounts are limited so that cap used does not exceed 30.00 USD by truncating the latter charge's discount to the remaining cap And monetary rounding is to 2 decimals in each currency; converted amounts for cap tracking round to 2 decimals using round half up And the ledger posts multi-currency entries with realized FX on refunds and reports discounts per currency and in the family's billing currency
Transparency and Auditability: Checkout, Invoice, Reports, and API
Given a transaction eligible for a sibling discount and cap When the user checks out and later views the invoice Then the UI and invoice show a distinct sibling discount line with rate, original amount, applied amount, cap used, and cap remaining, plus any proration details And admins can view an audit log entry for each discount calculation or recalculation including timestamp, triggering event (join, upgrade, downgrade, pause, cancel), inputs (prices, rates, cap before), outputs (applied discount, cap after), and actor/system And exports and APIs expose fields: discount_rate, discount_amount, cap_limit, cap_used_month_to_date, cap_remaining, prorated, effective_dates, currency, fx_rate_used, tax_base_after_discount
Admin Preview, Simulation, and Audit
"As an operations manager, I want to test sibling discount scenarios before going live so that I can avoid pricing mistakes and support escalations."
Description

Offer a configuration workspace where admins can draft rules, simulate carts for households with different numbers of children, and preview discount outcomes before publishing. Include stage vs production toggles, rule-effective date previews, and scenario testing for waitlist conversions and mixed carts (drop-ins, packs, memberships). Maintain an audit log of rule changes, previews, and publish actions with actor identity and timestamps to support compliance and troubleshooting. Provide role-based access controls for who can create, approve, and publish rules.

Acceptance Criteria
Admin drafts sibling discount rule and previews simulated cart outcomes
Given an admin with the Discount Manager permission is in the Sibling Saver configuration workspace (Stage), When they create a new sibling discount rule with stacking and cap parameters and save as Draft, Then the rule is persisted with status Draft and a version identifier. Given the draft rule, When the admin opens the Simulator and builds carts for households with multiple children across drop-ins, packs, and memberships, Then the simulator calculates and displays per-item and per-child discounts, applied caps, and total savings exactly as the rule defines, without altering live data. Given the simulation results, When the admin modifies rule parameters and re-runs the same scenario, Then the simulator recalculates and shows the updated outcomes, and both runs are retained in session for side-by-side comparison.
Stage/Production toggle and effective-date preview
Given Stage is selected, When an admin sets rule effective start/end date and account time zone for preview, Then all calculations in the simulator use the selected effective date/time to determine eligibility. Given the Stage/Production toggle, When the admin switches to Production preview, Then only Published rules are included in calculations and Draft/Approved (unpublished) rules are excluded. Given the toggle state, When the admin returns to Stage, Then no Production data is mutated and Stage-only simulations remain isolated from live checkout.
Simulate waitlist conversion and mixed carts
Given a simulated cart with a child on a waitlist item and other items (drop-ins, packs, memberships) for siblings, When the waitlist seat is simulated as converting to enrolled, Then discounts are recalculated according to current rules, stacking logic, and caps for the entire household cart. Given mixed cart items with different billing cadences, When the simulator runs, Then it shows how discounts apply to each item type (one-time, pack, recurring) and how caps are enforced per billing period as defined by the rule. Given a household with changing item quantities, When items are added or removed in the simulator, Then the recalculated totals and applied discounts update immediately without persisting any live transactions or inventory changes.
Audit log captures rule changes, previews, and publish actions
Given any rule is created, edited, previewed, approved, published, or rolled back, When the action is performed, Then an audit entry is recorded with actor identity, role, timestamp (UTC), environment (Stage/Production), action type, rule version, and a summary of field changes (before/after for edits) or simulation context for previews. Given the audit log, When an admin filters by rule, actor, date range, or action type, Then matching entries are returned and can be exported as CSV. Given the audit trail, When any user attempts to alter or delete an audit entry, Then the system prevents modification and logs the attempt as a security event.
Role-based access controls for rule lifecycle
Given role permissions are configured, When a user with Creator role accesses the workspace, Then they can create drafts and run simulations but cannot approve or publish. Given a user with Approver role, When they review a draft, Then they can mark as Approved and add an approval note but cannot publish unless they also have Publisher role. Given a user with Publisher role, When they attempt to publish a rule that is not Approved, Then the system blocks the action with a clear error; When they publish an Approved rule, Then the rule status becomes Published with version incremented, and the action is audited. Given separation-of-duties is enabled, When the same user created and approved the rule, Then the system requires a different Publisher to complete publication.
Publish validation, scheduling, and rollback
Given a rule in Approved status, When the admin schedules activation for a future date/time, Then the system validates conflicts with existing Published rules and blocks overlapping scopes unless a deactivation schedule for the superseded rule is provided. Given a scheduled activation, When the activation time is reached, Then the rule becomes Active in Production and appears in Production preview; simulations run against the active schedule. Given a published rule causes unintended outcomes, When the admin initiates rollback to a prior version, Then the prior version becomes Active and the rollback is logged with reason; existing invoices remain unchanged and only new checkouts use the reverted logic.
Preview transparency matches invoice breakdown
Given a simulation scenario that results in specific discounts and caps, When the same household performs a real checkout in Production after the rule is active, Then the invoice line items, discount descriptions, capped amounts, and totals match the simulator preview within $0.01. Given the simulator display, When a user expands a line item, Then it shows the discount source rule name/version, stacking order, cap logic applied, and a household-level summary consistent with invoice descriptors.
Reporting, Alerts, and Abuse Controls
"As a studio owner, I want visibility into sibling discount usage and safeguards against abuse so that I can sustain the program profitably."
Description

Deliver reports that track sibling discount usage, revenue impact, average savings per family, and top discounted products over time, with CSV export and API access. Provide per-family and per-period limit enforcement with configurable alerts for unusual activity (e.g., frequent relinking of guardians or abnormal discount totals). Generate webhook events for discount-applied and cap-reached to integrate with downstream systems, and allow authorized admins to override with required notes for audit. Implement basic anomaly detection and rate limits to reduce abuse without hindering legitimate families.

Acceptance Criteria
Discount Reporting Dashboard and CSV Export
Given an authorized admin selects a date range, When the Sibling Saver report is generated, Then it displays total discount count, total discount amount, net revenue impact, average savings per family, and the top 10 discounted products by discount amount for that range. Given invoices with sibling discounts exist in the selected range, When the totals are compared to invoice line items, Then the aggregates match within ±0.01 currency units and product counts match exactly. Given the organization has set a business timezone, When the dashboard renders, Then charts and on-screen timestamps use the org timezone and clearly display it. Given the user clicks Export CSV, When the export is generated for up to 50,000 rows, Then the file downloads within 10 seconds and contains column headers: date, family_id, invoice_id, product_id, product_name, product_type, discount_type, discount_amount, pre_discount_amount, post_discount_amount, cap_type, cap_period, cap_remaining, applied_by, override_flag, location_id, and timestamps in UTC ISO-8601. Given no data exists for the selected period, When the report loads, Then it displays zeroed metrics and a “No data” state without errors. Given a coach role without reporting permissions, When they attempt to view or export the report, Then access is denied and is logged in the audit log.
API Access to Discount Metrics
Given a valid API client with scope reports.read, When it requests the discount metrics endpoint with date_range, group_by (day|week|month|product|family), and pagination cursor, Then it receives a 200 response with totals, breakdowns, and a next_cursor when more data is available. Given the client sends an invalid or expired token, When it calls the endpoint, Then it receives 401 with an error code unauthorized and no data. Given the client requests up to 10,000 aggregated rows, When the server processes the request, Then the response time is ≤ 2 seconds 95th percentile and includes rate-limit headers (X-RateLimit-Limit, Remaining, Reset). Given OpenAPI documentation is requested, When the spec is fetched, Then the endpoint, parameters, response schema, and error codes are documented and match the actual behavior. Given the client filters by product_type and location_id, When the request is made, Then only matching aggregates are returned and totals reflect the filters.
Per-Family and Per-Period Cap Enforcement
Given a family with a monthly discount cap of $100, When cumulative sibling discounts applied in the current month reach $100, Then subsequent checkouts receive no additional sibling discount and the UI shows Cap reached with the remaining cap of $0. Given a purchase that would exceed the cap, When the discount is calculated, Then only the portion up to the cap is applied (partial discount) and the remainder is not discounted. Given a discount is denied due to cap, When the invoice is generated, Then it includes a line-level annotation Discount not applied: monthly cap reached and records cap_type, cap_period_start, and cap_period_end in metadata. Given concurrent checkout attempts from the same family, When discounts are computed, Then the cap is enforced atomically so total applied discount never exceeds the cap. Given admin-defined stacking rules, When multiple eligible discounts exist, Then the sibling discount is applied in the configured order and cap accounting reflects only the sibling discount portion.
Configurable Unusual Activity Alerts
Given alert thresholds are configured (e.g., guardian relinks ≥ 3 per 24h per family; discount total ≥ 3× family 30-day average), When activity crosses a threshold, Then an alert is created within 2 minutes with family_id, triggering metric, threshold, and recent activity samples. Given an alert is created, When notification channels are enabled (in-app and email), Then a single alert notification per family per 24h per rule is sent with a link to review details. Given an admin views an alert, When they acknowledge it, Then the alert status updates to Acknowledged with user_id and timestamp and is captured in the audit log. Given a false-positive suppression duration is set, When the same family re-triggers within the suppression window, Then no duplicate alert is generated and the attempt is counted in the alert history. Given role-based permissions, When a non-admin attempts to view or modify alerts, Then access is denied and logged.
Webhook Events: discount-applied and cap-reached
Given a sibling discount is successfully applied to an invoice, When the transaction is finalized, Then a discount-applied webhook is delivered within 5 seconds containing event_id, event_type, occurred_at (UTC), org_id, family_id, invoice_id, line_items, discount_amount, discount_rule_id, cap_context, and override_flag. Given a discount application is blocked or limited by a cap, When checkout completes, Then a cap-reached webhook is delivered with cap_type, cap_period, cap_limit, cap_consumed, and cap_remaining. Given a subscriber endpoint responds with 5xx or times out, When delivering webhooks, Then retries occur with exponential backoff for at least 24 hours and a unique idempotency key is included; successful 2xx stops retries. Given webhook signing is enabled, When the webhook is delivered, Then the X-CT-Signature header contains an HMAC SHA-256 of the payload using the shared secret and the payload validates within ±5 minutes timestamp tolerance. Given a test mode is enabled, When a test event is sent from the dashboard, Then a webhook with test=true is delivered without affecting production metrics.
Admin Override with Required Notes and Audit
Given a discount is blocked by cap or flagged by anomaly detection, When an authorized admin with permission discounts.override applies an override, Then the system requires an audit note of at least 15 characters and records user_id, timestamp, IP, prior state, and new state. Given an override is applied, When the invoice is issued, Then it displays an Override applied indicator and includes the audit note in admin-only invoice metadata. Given an override occurs, When webhooks are emitted, Then the related event includes override_flag=true and audit_reference_id. Given a user without override permission attempts to override, When they submit the action, Then it is rejected with 403 and an audit log entry is created. Given an admin chooses to revert an override before settlement, When they confirm, Then the system restores pre-override calculations, retains the original and revert audit entries, and updates reports accordingly.
Anomaly Detection and Rate Limiting Controls
Given a baseline window of the last 30 days per family, When today’s sibling discount total exceeds configured multiple (default 3×) of the family’s baseline average, Then the family is flagged for review without blocking checkout. Given repeated discount application attempts from a single family, When attempts exceed 10 per minute (default) on checkout or guardian relink actions, Then subsequent attempts are rate limited for 5 minutes with a clear error message that does not expose internal rules. Given a legitimate high-usage day (e.g., seasonal pack sale) is whitelisted by an admin, When anomaly checks run, Then whitelisted periods are excluded from flagging while still recorded in history. Given anomaly flags are generated, When reporting runs, Then the number of flagged families and resolutions (acknowledged, dismissed, escalated) are available as daily aggregates via dashboard and API. Given rate limiting is triggered, When monitoring inspects system health, Then median checkout latency remains ≤ 500 ms discount-calculation component and no more than 1% of legitimate transactions are blocked, as measured by allowlist audits.

Consent Cascade

When policies change, new waiver versions roll out to all linked guardians and dependents with a single e‑sign flow. Smart reminders and class gatekeeping ensure only compliant families can book or check in, maintaining studio compliance without repetitive paperwork.

Requirements

Policy Versioning & Rollout Controls
"As a studio owner, I want to publish a new waiver version and target it to specific classes and dates so that families see and sign the correct policy without manual tracking."
Description

Enable studios to create, version, schedule, and target waiver/policy templates with clear state transitions (Draft, Scheduled, Active, Retired). Each version includes an effective date, scope (all locations/classes or selected cohorts), change summary, and backward-compatibility rules determining when prior signatures are invalidated. Provide safe rollout options (preview, limited audience, rollback), and link each policy version to affected classes, bookings, and families. Ensure seamless migration for existing users and clear mapping of which bookings require re-consent.

Acceptance Criteria
Policy Template Creation & Versioning
Given a studio admin with policy management permissions When they create a new waiver/policy template and enter name, type, content, and a change summary Then version 1 is saved in Draft state with effectiveDate unset and no scope And version metadata records author and timestamp And missing required fields prevent save with field-level errors Given an existing template with an Active or Draft version When the admin creates a new version from it Then the version number increments by 1 And prior versions remain immutable and readable And a change summary is mandatory to save Given any template When the admin views version history Then all versions list number, state, effectiveDate (if any), author, change summary, and created/modified timestamps
Scheduling & State Transitions
Given a Draft version with required fields, defined scope, a future effectiveDate, and a selected compatibility rule When the admin clicks Schedule Then the version state updates to Scheduled And a job is queued to activate at the effectiveDate in the studio's timezone Given a Scheduled version reaches its effectiveDate When the scheduler runs Then the version state changes to Active And the previously Active version (if any) transitions to Retired Given a Scheduled version When the admin cancels scheduling before the effectiveDate Then the version returns to Draft and the activation job is removed Given a Draft version When the admin sets an effectiveDate in the past Then validation prevents scheduling and displays an error Given the state model When an invalid transition is attempted (e.g., Retired to Active without rollback) Then the system blocks the action with a clear error message
Scoped Rollout Targeting
Given a Draft version When the admin selects scope "All locations and classes" Then the scope is saved as global and preview counts include all classes and active families Given a Draft version When the admin selects specific locations, classes, or cohorts (e.g., tags, age groups, instructors) Then only those targets are included in the preview and subsequent rollout Given scope selection When no targets are selected Then saving or scheduling is blocked with a "scope required" error Given a defined scope When the admin opens Preview Impact Then the system displays counts of affected classes, bookings, guardians, and dependents and provides an export
Backward Compatibility Rules & Signature Invalidation
Given a Draft or Scheduled version When the admin selects "Backward-compatible" Then prior signatures on the previously Active version remain valid after activation for the defined scope Given a Draft or Scheduled version When the admin selects "Breaking change" Then prior signatures made before the new version's effectiveDate are marked invalid for the defined scope upon activation Given a booking for a class occurring after the effectiveDate within scope When the guardian previously signed a now-invalid version Then the booking is flagged "re-consent required" and the guardian must e-sign the new version during booking or check-in Given a booking occurring before the effectiveDate When the guardian has a prior signature Then no re-consent is required
Safe Rollout Controls (Preview, Limited Audience, Rollback)
Given a Draft version with scope When the admin runs Preview Impact Then no notifications are sent and no states change And the system simulates enforcement and shows would-be noncompliant counts Given a Scheduled version with "Limited audience" enabled and a selected cohort (e.g., 10% families or named cohort) When the version activates Then only the limited audience receives notifications and enforcement And metrics report compliance rates for that audience Given an Active version When the admin initiates Rollback within the allowed window Then the immediately previous Active version returns to Active And the rolled-back version moves to Retired And all signatures captured against the rolled-back version remain stored and auditable And notifications are sent to affected users about the rollback Given a rollback attempt When a previous Active version does not exist Then the system blocks rollback and displays a clear error
Linkage & Enforcement Mapping
Given a class session within scope after activation When a booking is initiated Then the system resolves the applicable Active policy version in under 200 ms and returns a compliance flag for the family (guardian and dependents) Given a policy version When queried via admin UI or API Then the system lists linked classes, bookings, and families with filters by date range, scope, and compliance status Given a family marked compliant for the Active version within scope When they attempt to book or check in Then the system permits the action without new consent Given a noncompliant family within scope When they attempt to book or check in Then the system blocks completion until the e-sign flow is completed, after which the booking/check-in proceeds
Migration & Re‑Consent Mapping for Existing Users
Given a new policy version becomes Active When activation completes Then a re-consent list is generated for all noncompliant families within scope and includes booking references and deadlines Given the re-consent list When reminders are scheduled Then guardians receive smart reminders per studio settings until compliance or opt-out, and reminder events are logged Given bookings created before activation but occurring after the effectiveDate within scope When the family is noncompliant after activation Then those bookings are flagged and surfaced to admins and to guardians with a required e-sign step before check-in Given families compliant via a backward-compatible rule When activation completes Then no reminders are sent and no booking blocks occur for those families
Guardian–Dependent Relationship & Eligibility Model
"As a parent, I want my signature to cover all my children in the selected studio so that I don’t have to sign the same waiver multiple times."
Description

Establish a robust data model linking guardians to dependents with support for multiple guardians per child, primary/secondary roles, custody preferences, and per-dependent consent requirements. Include age-of-majority rules by locale to determine self-consent for adults, and ensure that consent applicability propagates across classes and locations under the same studio brand. Provide privacy-safe management for adding/removing dependents and updating relationships, and compute real-time eligibility flags used by booking, check-in, and reminders.

Acceptance Criteria
Create and Manage Multi-Guardian Relationships
Given dependent D exists and guardian G1 is linked as primary When guardian G2 is linked to D Then D has exactly 2 guardians linked and only one primary (G1) and one secondary (G2) When a request attempts to mark a second primary for D Then the request fails with a validation error and no changes are persisted And all relationship changes are captured in an audit log with actor, timestamp, and before/after values
Custody Preferences Enforcement on Consent and Communications
Given dependent D is linked to guardians G1 and G2 with custody preferences canConsent(G1)=false, canConsent(G2)=true, receivesReminders(G1)=false, receivesReminders(G2)=true When G1 attempts to e-sign a consent for D Then the action is blocked with a permission error and no consent record is created When consent reminders are generated for D Then reminders are sent only to guardians with receivesReminders=true (G2) and delivery is logged And booking and check-in flows accept consents only from guardians with canConsent=true
Age-of-Majority Self-Consent by Locale
Given dependent D has DOB and locale L where age_of_majority(L)=A When the local time at L reaches 00:00 on D's A-th birthday Then D's consent mode switches to self-consent and guardian consent is no longer required for new bookings or check-ins And eligibility flags update within 5 seconds to reflect hasValidConsent based on D's signature of the latest waiver version When D's locale is changed to L2 with a different age_of_majority Then consent mode and eligibility flags are recalculated immediately according to L2
Consent Version Propagation Across Brand Locations and Classes
Given brand B has studios S1 and S2 and a waiver policy P with current version V When P is published at version V+1 Then all dependents under brand B are marked requiresConsent(P,V+1) for booking and check-in at S1 and S2 And booking or check-in attempts for noncompliant dependents fail with reason ConsentOutdated until P@V+1 is signed When a compliant consent P@V+1 is signed for dependent D by an authorized signer Then D becomes eligible to book and check in at all studios under B without additional signatures
Privacy-Safe Add/Remove Dependent and Relationship Updates
Given guardian G requests to add or remove a dependent or update relationship attributes When G lacks manageRelationships permission for that dependent Then the operation is denied and no PII is exposed When an authorized user unlinks a guardian from a dependent Then the dependent record remains intact, and the guardian’s PII is not visible to other guardians linked to the same dependent And if a dependent has no remaining guardians and no active obligations, the profile is soft-deleted and PII is redacted per retention policy while preserving minimal audit references accessible only to staff
Real-Time Eligibility Flags for Booking, Check-in, and Reminders
Given a dependent D and current relationship, custody, consent, DOB, and locale data When any of these inputs change or a new consent is signed Then eligibility flags (canBook, canCheckIn, consentStatus, signerType) are recomputed within 5 seconds and are available via API and UI And booking/check-in endpoints evaluate these flags synchronously and return deterministic reasons when blocked And reminder generation targets only dependents with consentStatus=Outdated or Missing and respects custody preferences
Unified E‑Sign Cascade Flow
"As a guardian, I want a single, guided signing experience that applies my consent to all linked dependents so that I can complete compliance quickly on any device."
Description

Deliver a single signing session that aggregates all pending policies for the guardian and selected dependents, applying signatures across the family in one pass. Provide responsive web and kiosk modes, prefill known data, and support identity verification (email/SMS OTP) as configured by the studio. Ensure legal compliance (ESIGN/UETA/eIDAS), accessible inline policy viewing, per-dependent acknowledgments when required, and branded UI. Generate a signed artifact per signer/dependent/policy version and store it securely with tamper-evident hashing.

Acceptance Criteria
Aggregated Policy Bundle Single Session
Given a guardian initiates consent from booking, dashboard, or reminder link and selects dependents A..N When the session starts Then the system aggregates all pending policy versions required for the guardian and each selected dependent, excluding any already compliant for the same policy version And displays the total count of items to sign and a per-policy list grouped by policy version and assignee And prevents completion until all required items in the bundle are signed And completes in a single session without requiring repeat signatures for the same policy version And computes the bundle in <=2 seconds for up to 10 dependents and 8 policy versions
Configurable Email/SMS OTP Identity Verification
Given the studio requires OTP via email and/or SMS When the guardian selects a permitted channel and requests a code Then a one-time code (6 digits) is delivered to the selected channel, valid for 10 minutes And up to 5 attempts are allowed before a 15-minute lockout And a maximum of 3 codes may be issued per 5 minutes per user per channel And successful verification gates access to the signing screens and is logged with method, timestamp, and masked destination And if OTP is disabled in studio settings, the session proceeds without OTP
Per-Dependent Acknowledgments Capture
Given a policy is flagged as requiring per-dependent acknowledgment When the unified session includes dependents D1..Dn Then the UI renders an acknowledgment control for each dependent for that policy, defaulting unchecked And the Continue/Complete actions remain disabled until all required dependent acknowledgments are provided And the resulting signed artifacts record the acknowledgment state per dependent with dependent ID and name And removing a dependent from the session removes their acknowledgment requirement
Accessible Inline Policy Viewing (WCAG 2.1 AA)
Given a policy needs review during signing When the guardian opens the inline policy viewer Then the exact policy version content is presented inline with a unique version identifier and last-updated date And the viewer supports keyboard-only navigation, screen-reader labels, focus order, and contrast meeting WCAG 2.1 AA And text reflows and remains readable at 200% zoom without horizontal scrolling on mobile And a Download/Print option is available and accessible
Signed Artifact Generation, Hashing, and Secure Storage
Given all required signatures and acknowledgments in the session are completed When the guardian submits the final confirmation Then the system generates a distinct signed artifact per signer/dependent/policy version containing policy ID and version, signer identity, dependent ID (if applicable), signature representation, consent to do business electronically, UTC timestamp, IP address, user agent, and verification method And a tamper-evident SHA-256 hash of the artifact payload is computed and stored And the artifact and metadata are stored encrypted at rest and are retrievable by authorized staff and the guardian And upon retrieval the stored hash matches a recomputed hash; any mismatch is flagged and access is denied
Responsive Web and Kiosk Modes with Branded UI
Given the session runs on web or kiosk When rendered on viewports 320px–1440px Then layouts adapt responsively with tap targets >=44px and forms usable on touch and keyboard And kiosk mode hides personal contact editing, disables browser navigation, includes a visible Cancel/Start Over, and auto-resets after 60s idle And studio branding (logo and primary/secondary colors) is applied to headers, buttons, and PDF artifacts without reducing contrast below WCAG 2.1 AA
Prefill Known Data and Profile Update Consent
Given the guardian has existing profile data When the session loads personal information fields Then known fields (name, email, phone) are prefilled with current values and pass validation (email format, E.164 phone) And edits to allowed fields require explicit confirmation to update the profile and are persisted only if confirmed And in kiosk mode, personal contact fields are read-only by default And field-level validation errors are shown inline and block submission until resolved
Smart Reminders & Escalation Engine
"As a studio manager, I want automated reminders that escalate if a family hasn’t signed so that I spend less time chasing paperwork."
Description

Automate multi-channel reminders (email, SMS, in-app) for outstanding consents with configurable cadence based on time-to-class and policy criticality. Pause reminders upon completion, and escalate to stronger messaging as deadlines approach. Provide studio-configurable templates, localization, quiet hours, and unsubscribe logic where applicable. Surface analytics on reminder effectiveness and deliver admin actions to resolve issues (resend, change guardian, remove dependent). All reminder events are logged for audit.

Acceptance Criteria
Configurable Multi-Channel Cadence by Time-to-Class and Policy Criticality
- Given a studio-defined cadence per policy criticality and time-to-class thresholds per channel (email, SMS, in-app), when a dependent lacks required consent for a booked class, then reminders are scheduled per the configured cadence and channel. - Scheduling is computed in the recipient’s local timezone and respects per-recipient per-day/per-hour caps as configured. - The schedule automatically re-computes if class start time, policy criticality, or recipient timezone changes. - For multiple dependents under the same guardian, reminders are consolidated per policy when enabled; otherwise sent per dependent without exceeding configured caps. - The system prevents duplicate sends within the same cadence window and logs any deduplication events. - Reminders are not scheduled past the configured cutoff before class or for disallowed channels per policy.
Pause and Cancel Reminders Upon Consent Completion
- When the guardian signs the required consent, all future scheduled reminders for that guardian/dependent/policy are immediately canceled across all channels. - No reminder with a send time greater than the recorded completion timestamp is delivered; in-flight messages are suppressed where provider supports cancellation and logged if not suppressible. - If consent is revoked or a new waiver version is published, scheduling re-evaluates and re-queues per the new state. - If the responsible guardian changes, pending reminders transfer to the new guardian and are canceled for the former guardian, with audit entries for both actions. - Cancellation and suppression events are recorded with timestamps, channels, template versions, and correlation IDs.
Escalation to Stronger Messaging Near Deadline
- As time-to-class crosses configured thresholds, the engine escalates by increasing frequency, switching to escalation templates, and/or adding channels (e.g., SMS) for high-criticality policies. - Escalation respects global and channel-specific send caps and quiet hours; at least one final notice is scheduled within the last allowed window before cutoff if permitted by configuration. - Final-notice templates and subjects/headers are used only within the configured final window; prior reminders use standard templates. - Upon consent completion, all escalation steps cease immediately and queued escalation messages are canceled. - Escalation path and decisions (thresholds crossed, caps applied) are recorded for audit.
Studio-Configurable Templates with Localization Support
- Admins can create, edit, preview, assign, and version templates per channel and policy criticality with placeholders for class name, start time, location, guardian name, dependent name, policy name/version, and action links. - Templates support multiple locales; the system selects the recipient’s preferred locale with fallback to the studio default when unavailable. - Template validation blocks save and/or send when required placeholders are missing or invalid; fallback templates are used if the assigned template is inactive at send time. - Changes to templates apply to reminders sent after the effective time of the new version; previously sent messages retain their original content in audit logs. - Merge variables are rendered correctly at send time; failed render attempts prevent send and are surfaced in analytics and audit.
Quiet Hours and Timezone Compliance
- Studios can configure quiet hours per channel; the engine does not send reminders during quiet hours in the recipient’s local timezone. - Messages that would fall within quiet hours are deferred to the next permissible window, preserving the configured cadence order. - Policy-level overrides can allow/deny quiet hour bypass for high-criticality policies; when overrides are disabled, legal and carrier restrictions are always respected. - Guardian timezone is resolved from profile; if absent, studio default timezone is used and logged. - All deferrals and timezone resolutions are recorded with original vs adjusted send times.
Unsubscribe and Communication Preferences Management (Where Applicable)
- Email and SMS reminders include compliant unsubscribe mechanisms (email link; SMS STOP/UNSUB keywords) when applicable to the policy; in-app follows app notification settings and does not present unsubscribe where not applicable. - Upon unsubscribe or STOP, the system immediately suppresses future reminders for that channel/category for the recipient and confirms the change; alternative channels may be used if allowed and not unsubscribed. - Admins with appropriate permissions can view/update a recipient’s communication preferences; resubscribe requires explicit opt-in and is captured with timestamp and source. - Mandatory transactional messages (as configured by the studio) are exempt from unsubscribe, are clearly marked as transactional in audit, and are excluded from opt-out suppression metrics. - Suppression decisions are visible in analytics and fully auditable per event.
Analytics, Admin Resolution Actions, and Audit Logging
- Analytics dashboard reports, by date range, policy, channel, and template: scheduled, sent, delivered, failed/bounced, opened, clicked, unsubscribe/STOP rate, and consent completions attributed within a configurable window. - Admins can perform resolution actions from analytics or reminder detail: resend a reminder, change responsible guardian, remove dependent from class, and retry failed sends; all actions require appropriate permissions and confirmation. - All reminder lifecycle events (schedule, update, defer, cancel, send attempt, delivery status, open/click, unsubscribe, completion, admin actions) are logged with timestamp, actor/system, channel, recipient, policy, class, template ID/version, and correlation ID; logs are immutable and exportable (CSV) with filters. - Audit views support filtering by guardian, dependent, policy, class, channel, status, and date range and return complete results consistent with the export. - Resend and admin actions update analytics in near real time and are linked to prior events via correlation IDs.
Booking & Check‑In Compliance Gatekeeping
"As front desk staff, I want the system to block non-compliant bookings and check-ins while offering a quick path to sign so that classes remain compliant without delays."
Description

Enforce real-time consent checks during booking, waitlist-to-payment automation, and class check-in. Block or warn based on studio rules and policy severity, and present an inline path to complete signing without abandoning the flow. Support staff override with reason codes and time-bounded grace periods, all fully audited. Provide offline-tolerant behavior at kiosks using last-known compliance status with queued sync. Offer QR links for on-the-spot signing to minimize front-desk delays.

Acceptance Criteria
Real-Time Booking Gate with Inline Signing
Given a guardian is booking a class for a dependent with one or more unsigned or outdated policy versions and the studio rule for those policies is set to Block When the guardian attempts to confirm the booking Then the booking is blocked, an inline signing flow opens listing all required policies with version numbers, and the user can complete signatures without leaving the booking flow And upon successful signing of all required policies, the booking auto-submits with the original class selection, attendee, price, and discounts preserved, and only one booking is created And the compliance check completes within 500 ms at p95 and surfaces a clear reason for the block Given the studio rule for at least one pending policy is set to Warn (and none set to Block) When the guardian attempts to confirm the booking Then the booking is allowed with a prominent warning banner and an inline option to sign now, and the warning is recorded with timestamp and user id Given multiple policies are pending across guardian and dependent When the inline signing flow starts Then all required signatures are bundled in a single e-sign session and marked complete atomically; partial completion does not confirm the booking
Waitlist-to-Payment Compliance Check
Given a dependent is auto-promoted from waitlist to an available spot When the system initiates payment capture or sends a pay-now link Then a compliance check is run first; if non-compliant, no charge is attempted and a signing link is sent to the guardian And the spot is held for the studio-configured hold duration (e.g., 2 hours) pending signature completion And if all required signatures are completed within the hold, payment proceeds automatically and the booking is confirmed without requiring the user to reselect the class And if the hold expires without completion, the spot is released and the guardian is notified; no partial charges are made Given policies are in Warn state only When auto-promotion occurs Then payment proceeds and a warning with a sign-now link is included in the notification, and the warning event is recorded
Class Check-In Gate with On-the-Spot QR Signing
Given a family arrives at class check-in and the attendee has pending Block-severity policies When staff or a self-service kiosk attempts to check in the attendee Then check-in is blocked and a QR code is displayed linking to a mobile-friendly signing session for all required policies And upon successful signing, the check-in view auto-refreshes and allows check-in without losing the attendee selection; confirmation occurs within 10 seconds of signature completion And if the policies are Warn-only, check-in is allowed with a visible warning and a one-tap prompt to sign now And if the attendee has multiple dependents queued, only the non-compliant dependent is blocked while compliant dependents can proceed
Staff Override with Reason Codes and Grace Period
Given a staff member with Override permissions views a non-compliant attendee When they apply an override Then they must select a reason code from a studio-configured list and optionally enter a note; the override is created with a time-bounded grace period (start and expiry timestamps) And during the grace period, bookings, waitlist promotions, and check-ins for the attendee bypass Block-severity policies but still surface warnings And overrides cannot be applied to policies marked Non-Overridable by the studio; the UI prevents selection and explains why And staff can revoke an active override; revocation immediately restores normal gatekeeping And all override actions (grant, modify, revoke) are fully audited with user id, role, timestamp, IP/device, policy versions, reason code, and grace window
Offline Kiosk Gatekeeping with Last-Known Status and Queued Sync
Given a check-in kiosk is offline When a non-compliant attendee attempts to check in Then the kiosk displays an offline indicator with last-synced timestamp and applies last-known compliance statuses And if the last sync age is within the studio-configured freshness window, decisions follow the cached statuses; if stale beyond the window, the kiosk applies the studio-configured fallback (Block or Warn) and labels the outcome as Pending Verification And the kiosk generates a QR code that opens a signing session via the attendee’s mobile device; the kiosk records the attempt and queues a pending check-in And upon reconnection, the system syncs queued check-ins and reconciles with actual signature status; compliant items auto-complete check-in, while conflicts are flagged for staff review
Comprehensive Audit Logging of Gate Decisions
Given any gate decision occurs (booking attempt, waitlist promotion, check-in, override grant/revoke) When the event is processed Then an immutable audit record is created within 2 seconds containing: actor (user/staff/system), target attendee/guardian, device (web, kiosk, staff app), location (if available), policy ids and versions evaluated, configured severities, decision (Allow/Warn/Block), path taken (inline sign/override/queued), reason code (if any), grace window (if any), and correlation ids for related booking/check-in And audit records are viewable in the admin UI with filter/search by date range, policy version, attendee, decision, and exportable to CSV And audit records are append-only and cannot be edited; corrections are recorded as new events linked by correlation id
Compliance Check Resilience and Timeouts
Given the compliance service is reachable When a booking, promotion, or check-in triggers a compliance evaluation Then the evaluation completes within 500 ms at p95 and 1.5 s at p99, and UI state is preserved during any retry Given the compliance service times out or returns an error When a gate decision is required Then the system applies the studio-configured fail-safe (Block or Warn), displays a clear message, and queues a background retry; no payment is attempted until compliance is confirmed And if a user completes signatures while a previous attempt is pending, the next retry observes the updated status and proceeds without duplicate charges or duplicate bookings
Compliance Audit Trail & Reporting
"As a studio owner, I want downloadable proof of consent and an at-a-glance compliance status so that I can satisfy insurance and regulatory audits."
Description

Maintain an immutable, exportable audit trail capturing consent events (who signed, for whom, policy version, timestamp, IP, device), reminders sent, overrides granted, and gating decisions. Generate and store signed PDFs with cryptographic hashes and provide search/filter by family, class, date, and policy version. Deliver dashboards showing compliance coverage at studio, class, and family levels, plus CSV exports and read-only API endpoints for insurers and regulators. Support data retention and subject rights (GDPR/CCPA) with secure, encrypted storage.

Acceptance Criteria
Immutable Consent Event Logging
- Given a consent is captured via Consent Cascade, when the signer submits the e‑sign form, then an audit event is created with fields: event_id (UUIDv4), family_id, guardian_id, dependent_id (nullable), policy_id, policy_version, action=consent_signed, timestamp (ISO 8601 UTC), signer_name, signer_email, IP (IPv4/IPv6), device_user_agent, device_fingerprint (if available). - Then the event is written to an append-only store; updates are disallowed and any correction creates a new event linked via previous_event_id. - Then the event is linked to class/session context when signing occurs during booking or check-in. - Then the event includes content_hash (SHA-256 of the signed PDF) and previous_hash to enable tamper-evident hash chaining.
Signed PDF Generation & Verification
- Given a consent is signed, when the transaction completes, then a signed PDF is generated embedding policy text/version, signer identity, dependent(s) covered, timestamp, IP, and e‑sign evidence. - Then a SHA‑256 hash of the exact PDF bytes is computed and stored with the audit event; recomputing the hash on download matches the stored hash. - Then the PDF is stored encrypted at rest and available for authorized download within 5 seconds of signing. - Then a verification endpoint returns the stored hash and verification status for a provided PDF; mismatches are logged as integrity_alert events.
Reminders, Gatekeeping, and Overrides Logged
- Given a policy update triggers Consent Cascade, when reminder notifications are sent, then reminder_sent events are logged with recipient, channel (email/SMS/push), template_id, send_timestamp, and delivery_status. - When a booking or check-in is attempted without current consent, then a gatekeeping_denied event is recorded with reason, rule_id, class_id/session_id, family_id, and the API/UI responds with 403 and error code CONSENT_REQUIRED. - When an admin grants a one-time override, then an override_granted event is logged with admin_id, scope (booking/check-in), class_id/session_id (or global), expiry_timestamp, and justification; auto-expiry emits override_expired.
Audit Search and Filter
- Given an authorized staff user opens the audit page, when filters are applied by family, class/session, date range, and policy_version, then results return within 2 seconds for up to 10k events and include total count with pagination (limit/offset or cursor). - Then results can be sorted by timestamp ascending/descending and exported without altering the underlying audit data. - Then only records within the user's studio tenant are returned; cross-tenant access attempts are blocked and logged.
Compliance Coverage Dashboards
- Given current audit data, when viewing the studio dashboard, then it displays compliance coverage % of active families on the current policy version, count of non-compliant families, and a 30‑day trend line. - When viewing a class/session, then it shows counts of compliant, pending, and blocked attendees and flags classes with >10% non-compliance. - When viewing a family profile, then it shows current consent status per dependent and last signed version. - Then dashboard tiles load within 3 seconds for studios up to 5k families and can be exported as PNG/PDF.
CSV Export and Read-only Auditor API
- Given an authorized user with export permission, when exporting, then a CSV is generated with selected fields for consent events, reminders, overrides, and gating outcomes using UTF‑8, RFC 4180 formatting, stable headers, and ISO 8601 UTC timestamps. - Then exported datasets over 50k rows are streamed and complete without timeouts, and a checksum (SHA‑256) of the CSV is provided. - Given an auditor API token, when calling read-only endpoints, then responses enforce read-only scope, apply rate limiting (≥60 req/min), return 200 for valid and 401/403 for invalid/expired tokens, and support PII redaction (mask emails/IPs) indicated in response metadata.
Data Retention, Encryption, and Subject Rights
- Given a studio retention policy (e.g., 7 years), when thresholds are reached, then events are purged or anonymized per policy while preserving hash-chain integrity; actions emit retention_purge events. - Given a data subject access request, when initiated, then a complete export of the subject's audit records and PDFs is produced for admins within 30 days and logged as dsar_fulfilled. - Given a deletion request with legal hold constraints, when processed, then personal identifiers are anonymized/pseudonymized while retaining event structure; actions are logged as deletion_completed with legal_hold_reference when applicable. - Then all audit data and PDFs are encrypted at rest (AES‑256) and in transit (TLS 1.2+), with keys rotated at least annually and access to keys and exports fully logged.

Family QR Check‑In

One QR covers the whole family: scan once to mark the right kids present for the right classes, even across rooms. Late/absence notes and pickup codes capture in seconds, reducing lines and attendance errors while syncing to each child’s roster instantly.

Requirements

Family QR Code Generation & Lifecycle Management
"As a parent/guardian, I want a single QR that represents my family so that checking in multiple children is fast and consistent."
Description

Issue a single, brandable QR code per family account that encodes a secure, rotating token linked to all enrolled children and their upcoming classes. Support automatic regeneration on membership changes, configurable expiration/rotation, and revocation. Provide distribution via email, SMS, and wallet pass, with admin controls to view/regenerate and families able to add to device wallets. Enforce multi-tenant isolation, rate limiting, and tamper detection to prevent reuse or forgery. Include environment support (staging/production), versioning, and analytics on scans.

Acceptance Criteria
Single Brandable Family QR Generation
Given a family account exists with at least one enrolled child in a tenant When a QR is generated for the family for the first time Then the system creates exactly one QR code asset per family within that tenant And the QR image is rendered with the tenant’s branding settings (logo/color theme) And the QR encodes a secure, non-PII token that resolves to the family and all currently enrolled children and their upcoming classes And repeated generation requests return the same active QR/token unless rotation or regeneration is triggered
Secure Rotating Token, Expiration, and Tamper Detection
Given the tenant’s configuration defines token rotation and expiration intervals When the rotation interval elapses Then a new token is generated and the old token expires according to the configured policy And expired tokens are rejected with an explicit "expired" outcome When a scanned token fails signature/nonce validation or has been altered Then the scan is rejected with a "tampered" outcome and the event is logged And tokens include issued-at, expiry, tenant-id, and version to prevent replay and cross-tenant reuse
Automatic Regeneration on Membership or Enrollment Changes
Given a family has an active QR token When a child is added or removed from the family account, a class enrollment is created/updated/canceled, or the membership status changes Then the system regenerates a new active token within 60 seconds And previously active tokens are invalidated immediately after regeneration And the current QR image and wallet pass reflect the new token without requiring a new pass install
Revocation and Immediate Invalidation Controls
Given an admin with permission selects "Revoke QR" for a family or the system flags the account as compromised When revocation is confirmed Then all active tokens for that family become invalid within 15 seconds And any subsequent scans return a "revoked" outcome And the admin can optionally trigger reissue, which produces a new token and QR And all actions are audit-logged with actor, timestamp, and reason
Multi-Channel Distribution: Email, SMS, and Wallet Pass
Given a new or regenerated family QR is available When distribution is triggered automatically or by an admin Then an email is sent to the family’s primary address containing the QR image/link And an SMS is sent to the family’s verified phone with a secure link to the QR And the family can add the pass to Apple Wallet and Google Wallet from the link And the wallet pass displays the QR and updates automatically on token rotation or regeneration And delivery failures (bounce, undeliverable, opt-out) are surfaced to admins with retry options
Admin Controls, Role Permissions, Tenant Isolation, and Rate Limiting
Given role-based permissions are configured When an admin views a family profile within their tenant Then they can view the current QR thumbnail, see last rotation/regeneration timestamps, and trigger regeneration or revocation per permissions When an admin from a different tenant attempts access Then access is denied and logged When scan or token-lookup requests exceed configured thresholds by IP/device/family Then requests are rate limited and subsequent scans within the window return a "rate_limited" outcome
Environment Parity, Versioning, and Scan Analytics
Given staging and production environments are configured When a token issued in staging is scanned in production (or vice versa) Then the scan is rejected with an "environment_mismatch" outcome And each token contains a version identifier used by the scanner to handle decoding When a scan occurs Then analytics capture tenant, family (internal id), token version, environment, outcome, timestamp, and scanner client metadata without storing PII And aggregated scan metrics are available to admins with filters by date range, tenant, outcome, and environment
Multi-Child Cross-Room Check-In Workflow
"As front-desk staff, I want to scan once and mark each child’s attendance across their classes so that lines move quickly and rosters update correctly."
Description

Upon scanning a family QR at any check-in device, display all children with classes within a configurable time window, grouped by class and room. Allow one-tap marking of Present, Late, or Absent per child per class, with visual confirmation, sound, and prevention of double-bookings. Instantly sync updates to each class roster and instructor view, resolving cross-room attendance without switching screens. Support concurrent classes, capacity checks, and debounced repeat scans. Enforce role-based permissions and configurable cutoff rules.

Acceptance Criteria
Cross-Device Family QR Scan Lists Eligible Children by Class/Room
Given an authenticated staff user and a configured time window (T_before, T_after) When a valid family QR is scanned on any check-in device at time t Then within 1.5 seconds the screen lists all and only the family's children who have classes with start times in [t − T_before, t + T_after], grouped by class and room, and each child/class row shows child name, class name, room, start time, and current attendance state And the groups are sorted by start time ascending and then room name; children within a group are sorted by last name ascending And if no eligible classes exist, the UI shows “No eligible classes in the current window” within 1 second And scanning the same QR on different devices at the same timestamp yields the same list and grouping
One-Tap Present/Late/Absent with Immediate UI Feedback
Given eligible children are listed for the scanned family When the staff user taps Present, Late, or Absent for a specific child/class Then the selected state applies with a color-coded badge and a confirmation tone within 300 ms, and the button shows a checkmark And only one state can be active per child/class; tapping another state switches to it; tapping the active state again clears it And the action is temporarily debounced locally by disabling the tapped control for 800 ms to prevent accidental double taps And a confirmation toast displays child name, class, room, selected status, and timestamp
Overlap Conflict: Prevent Double-Booking Across Concurrent Classes
Given a child has two or more classes whose times overlap by any amount within the current window When the child is marked Present in one of the overlapping classes Then the Present option for the other overlapping classes becomes disabled with an inline message “Conflict—Present in [Class A]”; Late and Absent remain available And attempting to force Present in a conflicting class is rejected with an error toast and no state change And conflict enforcement is reflected across all devices within 2 seconds
Real-Time Sync to Rosters and Instructor Views
Given a child/class attendance state is set or changed from the check-in device When the action is saved Then the corresponding class roster and instructor live view reflect the new state within 2 seconds on connected devices And the roster counts (Present, Late, Absent) update consistently and match the check-in device state
Capacity Enforcement with Override Permissions
Given a class has capacity C and a current Present count P When marking Present would result in P + 1 > C Then the action is blocked and a “Capacity full” message is displayed within 500 ms; the child’s state does not change And if the user has Capacity Override permission, an explicit confirmation allows proceeding; without the permission the override option is not shown And marking Present for waitlisted children is blocked unless capacity allows; Late/Absent can still be recorded
Debounced Repeat Scans Are Idempotent
Given a family QR was scanned and the session is active When the same QR is scanned again on any device within the configured debounce window D seconds Then no duplicate entries or duplicate actions are created; the existing session view is brought to the foreground and displays “Already scanned at HH:MM” And scans after D seconds reopen the latest state without replaying prior actions
Role-Based Permissions and Check-In Cutoff Rules
Given role-based permissions and class-level check-in cutoff rules are configured When a user without Check-In permission scans a family QR Then the eligible class list is view-only and attendance buttons are disabled with a tooltip “Insufficient permissions” And when current time is beyond a class’s configured check-in cutoff, Present is disabled or hidden per configuration and only allowed statuses (e.g., Late/Absent) are available with an explanatory note And users with Override Cutoff permission can apply Present after cutoff only after confirming a dialog; otherwise the action is blocked
Late/Absence Notes Quick Capture
"As front-desk staff, I want to capture late or absence reasons during check-in so that instructors have context and records stay accurate."
Description

Enable fast capture of standardized late/absence reasons, expected arrival times, and optional free-text notes during check-in. Store notes on the attendance record with timestamps, surface them to instructors and guardians, and include them in daily rosters and exports. Provide configurable reason templates, keyboardless quick-pick UI, and localization. Support editing with audit logging and notifications when lateness exceeds defined thresholds.

Acceptance Criteria
Quick-Pick Late/Absence Capture at Family QR Check-In
Given a guardian scans a valid family QR at a check-in device during active class windows And the family has 1–5 children with scheduled classes within the next 60 minutes across one or more rooms When the check-in UI presents each child with Present, Late, Absent quick-pick options Then selecting Present marks the child present in the correct class roster within 2 seconds And selecting Late requires choosing a standardized reason from the configured list and an expected arrival time using a tap-only control; free-text note is optional And selecting Absent requires choosing a standardized reason; free-text note is optional and limited to 250 characters And all inputs are captured without invoking a keyboard on tablet-sized devices Then each submission is timestamped, linked to the specific class attendance record, and visible in the session audit And the flow supports completing check-in for up to 3 children in under 10 seconds total on a typical kiosk (p50), with p95 under 20 seconds
Input Validation for Expected Arrival and Notes
Given Late is selected for a child When an expected arrival time is chosen Then it must be between now and the class end time; otherwise the UI displays a validation message and prevents save And if the class ends within 5 minutes, expected arrival is optional and defaults to class end time And free-text notes cannot exceed 250 characters and must not contain only whitespace And the reason must be selected from the active template list; custom reasons are not allowed
Configurable Reason Templates
Given an org admin creates, edits, reorders, activates, deactivates, or localizes late/absence reason templates When changes are saved Then the updated templates propagate to all check-in clients within 60 seconds or on next reload, whichever comes first And deactivated templates no longer appear at check-in but remain visible on historical records as the saved label snapshot And templates can be scoped by location and class type; check-in shows only templates applicable to the child’s class And one template can be set as default per status (Late/Absent) and is preselected when that status is chosen
Localization of Reasons and UI
Given the check-in device locale is set or the organization default locale is configured When the check-in UI and reason templates are displayed Then all labels, prompts, and reason names appear in the selected locale And if a translation is missing for a reason, the system falls back to the org default language And right-to-left scripts render correctly; tap targets remain aligned and accessible per WCAG AA touch target sizes
Surfacing Notes in Rosters, Guardian Views, and Exports
Given a late/absence note is captured for a child When an instructor opens today’s roster on web or mobile Then the status (Present/Late/Absent), standardized reason, expected arrival time, free-text note, and timestamp are visible next to the child’s name with an icon indicator And when a guardian views the check-in confirmation or today’s schedule, they can see the captured status, reason, and expected arrival time for their own child(ren) And daily roster PDFs and CSV exports include columns: Status, Reason, Expected Arrival (local time), Note, Captured By, Captured At (ISO 8601) And export values match the attendance record exactly and are timezone-correct
Editing, Audit Log, and Threshold-Based Notifications
Given staff with Edit Attendance permission opens a child’s attendance record with a Late/Absent entry When the reason, expected arrival, or note is edited Then the system records an audit entry with previous value, new value, user, timestamp, and source (kiosk, web, mobile) And edits are allowed until 24 hours after class end unless overridden by an admin And when expected arrival exceeds the class lateness threshold defined in settings (e.g., 15 minutes), the system triggers a notification to the instructor dashboard and optionally to guardians based on org notification settings And notifications are sent at most once per child per class per threshold crossing and are suppressed if the child is later marked Present before the threshold time And changing expected arrival re-evaluates threshold rules and may send or cancel notifications accordingly
Pickup Code Capture & Verification
"As front-desk staff, I want to verify or issue pickup codes at check-in so that end-of-class releases are secure and efficient."
Description

At check-in, generate or validate per-child pickup codes tied to authorized guardians for the session. Support numeric and QR pickup codes, one-time or daily validity, and quick lookup at pickup. Log verification events with staff, device, and time, and flag mismatches with override workflows and reasons. Expose codes in parent portal, and integrate with instructor and front-desk views for end-of-class release. Provide APIs/webhooks for door access or third-party security systems.

Acceptance Criteria
Per‑Child Pickup Code Generation at Check‑In
Given a family QR is scanned and children are marked present for their sessions When the check-in is confirmed Then the system generates a unique pickup code per child according to organization configuration (numeric or QR; one-time or daily) And each code is linked to the child and the session’s authorized guardian list And codes are available in staff and parent views within 2 seconds of check-in And codes are unique within the facility for the active validity window
Pickup Code Validation and Guardian Match at Release
Given a staff member opens the release screen for a class When a presented pickup code is scanned or manually entered Then the system matches the code to a single child in that class and verifies the code is tied to an authorized guardian for that child for the session And the screen displays the child’s name, photo, and session details for confirmation And the system records a verification event containing staff ID, device ID, timestamp, event type "Verify", and outcome "Success" And validation fails with a clear reason if the code is expired, already used (for one-time), or not valid for this class
Override Workflow on Code Mismatch
Given a code is entered that does not match the child/session or is not tied to an authorized guardian When a staff member with override permission attempts release Then the system requires selecting a reason from a configurable list and optionally entering notes And requires staff re-authentication before confirming the override And logs an override event with staff ID, device ID, timestamp, child ID, session ID, reason, and notes And marks the child’s pickup status as "Overridden" in staff views
Daily vs One‑Time Code Validity
Given organization validity is set to "Daily" and a code was generated at check-in When the code is validated multiple times on the same calendar day for that child’s session(s) Then each validation succeeds And validations after the day changes are rejected as "Expired" Given organization validity is set to "One-time" and a code was generated at check-in When the code is validated once for pickup Then subsequent validation attempts on the same day are rejected as "Already used"
Parent Portal Code Visibility & Access Control
Given an authorized guardian is signed into the parent portal/app When a child is checked in to a session Then the portal shows the child’s active pickup code in numeric and QR form and its validity type And it hides pickup codes for children the guardian is not authorized for And it hides or indicates "No active code" when the child is not checked in And updates the displayed code within 5 seconds of generation
Instructor/Front‑Desk Release View with Real‑Time Status & Lookup
Given a staff member opens the release view for a class When they search by child name, guardian name, enter the last 4 digits of a code, or scan a QR code Then matching results return within 1 second and show each child’s pickup status (Pending, Verified, Overridden) And the view accepts QR scans and numeric entry for validation And status updates are reflected across all staff devices within 2 seconds
Security System Integration via API/Webhook
Given webhooks are configured for the location When a pickup verification or override event is recorded Then the system sends a POST within 3 seconds including child ID, session ID, guardian reference, staff ID, device ID, event type, outcome, timestamp, and code type And retries failed deliveries with exponential backoff for up to 24 hours And exposes a secure API endpoint to validate a pickup code that returns success/failure, reason, and child/session references, requiring API key or OAuth 2.0
Payment & Eligibility Gate at Check-In
"As a studio owner, I want payment and eligibility checked at scan time so that unpaid attendance is handled without disrupting check-in."
Description

Validate enrollment, passes, and payments for each child’s class at scan time. When eligibility is missing or expired, present streamlined options to charge a drop-in, apply a pass, or send a mobile payment link without blocking the line. Respect studio policies (allow/deny pending check-in), record pending balances, and reconcile automatically once payment clears. Update roster and billing in real time and log all decisions for compliance and reporting.

Acceptance Criteria
Multi-Child Scan: All Eligible -> Instant Check-In
Given a family QR with multiple children whose classes start within ±2 hours and each has valid enrollment, pass, or prior payment When the QR is scanned at the check-in device Then eligibility for each child/class is validated within 500 ms per child (max 2 seconds total) And each eligible child is marked Present in the correct class and room And no payment prompts are shown And instructor rosters reflect the Present status in real time within 2 seconds And a confirmation list displays each child’s initials, class, and room
Missing/Expired Eligibility -> Non-Blocking Payment Options
Given at least one child in the scanned family lacks eligibility (no enrollment, expired/depleted pass, or unpaid balance) for the target class When the QR is scanned Then a single prompt surfaces options: Charge Drop-In, Apply Pass, Send Payment Link, and Skip for Now (if policy allows) And the scan station remains ready to scan the next family within 1 second regardless of prompt state And staff can defer the decision in ≤2 taps without losing the scan context And default drop-in price and class details are prefilled per studio pricing rules And only the affected child(ren) are flagged while eligible siblings are checked in
Apply Existing Pass at Check-In
Given the child has a valid unused pass applicable to the class per pass rules (type, date range, exclusions) When staff selects Apply Pass Then the pass is decremented exactly once and the child is marked Present immediately And billing reflects pass usage in real time And concurrent device scans do not double-deduct (idempotent operation) And an audit record captures pass ID, actor, device, timestamp, and before/after balance
Send Mobile Payment Link and Record Pending Balance
Given a guardian contact with SMS or email is on file and a child lacks eligibility When staff selects Send Payment Link Then a secure link is delivered via the preferred channel within 5 seconds, prefilled with child, class, and amount And a pending balance is recorded and visible on the child profile and class roster And link expiry, reminders, and retry limits follow studio settings And staff can proceed to the next scan immediately without blocking And the child’s attendance reflects policy (Payment Pending or Not Checked-In)
Studio Policy Enforcement: Allow Pending vs Deny Check-In
Given studio policy is Allow Pending Check-In When a child lacks eligibility at scan time Then the child is marked Present with a Payment Pending badge and the pending balance is recorded And instructor rosters display the pending badge Given studio policy is Deny Check-In When a child lacks eligibility at scan time Then the child is not marked Present and a Denied with Reason message is shown And a manager override with PIN (if enabled) allows a one-time Present with full audit logging
Automatic Reconciliation After Payment Clears
Given a pending balance exists for a specific child and class created at check-in When payment is captured (card on file, pass added, or link paid) Then the pending balance is closed and the ledger/invoice update within 5 seconds And the roster status changes from Payment Pending to Present automatically And a receipt is sent to the guardian and instructor views refresh And idempotency prevents duplicate charges or double attendance updates
Audit Trail and Reporting for Check-In Decisions
Given any check-in event involving eligibility validation When the scan and subsequent actions occur Then the system logs family QR hash, child ID, class ID, policy applied, decision taken (eligible, drop-in charged, pass applied, link sent, denied), actor/device, timestamps, amounts, and payment auth IDs And logs are immutable, filterable by date range/class/child, exportable to CSV, and accessible to admins And each event has a unique trace ID that correlates roster changes with billing entries
Resilient Offline Scanning & Sync
"As staff, I want check-in to keep working offline so that classes can start on time during network outages."
Description

Allow check-in devices to operate without connectivity, caching family tokens and today’s rosters locally with encryption. Queue scan events, notes, and status changes for background sync with conflict resolution and user prompts on reconnection. Provide clear offline/online indicators, prevent duplicate or out-of-window check-ins, and safeguard PII. Include administrative tools to purge local data and monitor sync health.

Acceptance Criteria
Offline Family QR Scan with Local Cache
Given the device has previously fetched and encrypted todays rosters and family tokens while online When the network is unavailable and a valid family QR is scanned Then the app resolves the family to all rostered children for the current day across rooms within 300 ms and displays selectable check-in targets Given multiple children are rostered in different concurrent classes When the operator selects which children to check in and confirms Then the app records presence per child-class with device timestamp, includes any entered late/absence notes and pickup codes, and adds events to the local sync queue Given the device is offline When the operator attempts to scan an unknown or expired token Then the app rejects the scan with an error explaining offline verification limitations and does not create queue entries Given cached rosters are older than 24 hours (or configured threshold) When attempting to check in Then the app displays a stale-data warning and requires an admin override to proceed
Offline Event Queue Persistence and Deduplication
Given the device is offline When check-in, late/absence note, and pickup code updates are performed Then each action is appended to a durable local queue with child ID, class ID, action type, and timestamp, and survives app kill/restart and device reboot Given the same child-class is checked in more than once while offline When the event is enqueued Then the app deduplicates to a single check-in event, preserving the latest note and pickup code Given the offline queue exceeds the configured capacity (default 5,000 items) When additional actions are attempted Then the app displays "Offline queue full" and prevents new offline actions until space is freed or synced Given there are pending queued actions When the operator views the queue status from the check-in screen Then the app shows a count of pending items and the age of the oldest item
Reconnection Sync and Conflict Resolution
Given pending queued actions exist When connectivity is restored Then the app automatically begins background sync within 5 seconds, processing events in chronological order and marking each as synced only after server acknowledgment Given a conflict is detected (e.g., child already checked in, class canceled, window closed, roster change) When syncing the affected event Then the app pauses that event, surfaces a human-readable prompt with recommended resolution options (apply as late, skip, change status), and resumes sync after selection Given multiple conflicts occur When the operator selects "Apply recommended for all" Then the app uses predefined conflict rules and generates a summary report upon completion Given some events fail due to server or validation errors When sync completes Then the app flags failures with retry options and does not mark them as synced
Duplicate and Out-of-Window Prevention Offline
Given offline mode When attempting to check in a child for a class outside the configured check-in window Then the app blocks the action and shows the allowed window Given offline mode When scanning a family QR where one or more children are already marked present for that class in the local cache Then the app prevents duplicate check-ins and shows which records are already present Given offline mode and role-based override is enabled When an authorized admin attempts an out-of-window check-in Then the app requires an override PIN and logs the override to the sync queue
Offline/Online Status Indicators and Prompts
Given the check-in screen is open When network connectivity changes Then a persistent status indicator clearly shows Online (green) or Offline (amber) within 1 second of change Given offline mode When a scan is processed Then the app displays "Saved offline" confirmation with the current offline queue count Given connectivity is restored and pending items exist When the check-in screen is active Then the app shows a prompt "Syncing X items..." and transitions to "All caught up" upon completion Given the cache for today is not yet downloaded When entering offline mode Then the app displays "Offline not ready—download todays rosters" with a one-tap fetch action
Admin Purge and Sync Health Dashboard
Given an admin with proper permissions When selecting "Purge Local Data" Then the app requires a two-step confirmation and an admin PIN, and upon confirmation removes all cached rosters, tokens, queued events, and logs from local storage irreversibly Given purge completes When inspecting the device storage via the app Then no PII is readable, and the check-in screen requires a fresh online fetch before next use Given an admin opens Sync Health When viewing the dashboard Then it displays device ID, last successful sync time, pending count, last error (if any), and time since roster cache refresh Given Sync Health is open When tapping "Export Log" Then the app produces an anonymized sync log file suitable for support
Encrypted Local Storage and PII Safeguards
Given any roster, family token, or queued event is stored locally When inspecting storage at rest Then data is encrypted using platform keystore-backed AES-256, with keys non-exportable and bound to the app install Given the device is locked When the app is in the background Then sensitive data is inaccessible, background screenshots are masked, and no PII is visible in the system app switcher Given the user logs out or an admin triggers purge When the operation completes Then all encryption keys and secured blobs are destroyed, rendering data unrecoverable Given a rooted/jailbroken device is detected When the check-in module starts Then the app blocks offline caching and displays a security warning, allowing online-only mode
Attendance Audit Trail & Reporting
"As a studio manager, I want a complete attendance audit trail and reports so that I can investigate issues and improve operations."
Description

Record an immutable, searchable history of all check-in actions, including scan source, staff/device identifiers, timestamps, pre/post values, notes, payment gates, and overrides. Provide filters, exports, and a reconciliation view to correct errors with permissioned rollbacks that retain audit entries. Surface KPIs such as no-show rate, late arrivals, average queue time, and error rates, and enable scheduled reports to stakeholders.

Acceptance Criteria
Immutable Audit Log on Family QR Check-In and Update
Given a staff member scans a Family QR code to check in multiple children across classes When the check-in is confirmed Then the system creates one audit entry per child with fields: child_id, class_id, roster_id, action_type=check_in, scan_source=QR, staff_id, device_id, location_id, event_time ISO 8601 with timezone, pre_value=absent, post_value=present, notes (if provided), payment_gate_status (paid/owed/skipped), override_flag=false, request_id idempotency key Given an attendance status is updated after initial check-in (e.g., late note added, status corrected, override applied) When the change is saved Then a new audit entry is appended with action_type=update capturing pre_value and post_value, override_flag true if override, and original entries remain unmodified Given connectivity is offline at scan time When the device syncs Then audit entries are created with the original scan timestamp, flagged scan_source=offline_sync, and ordering is by event_time, not process_time
Search and Filter Audit Trail
Given a studio admin opens the Audit Trail with >50k records When they filter by date range, class, child, staff, device, scan_source, action_type, override_flag, and notes keyword Then results return only matching entries within 2 seconds and support pagination of 100 rows per page Given a search term matches multiple fields When the user specifies a field-scoped search (e.g., child_name:"Sam") Then only that field is searched; otherwise default search spans notes and child names Given a studio uses a non-UTC timezone When filtering by date range Then the UI applies the studio’s timezone for input and display, with UTC shown on hover for each timestamp
Reconciliation View with Permissioned Rollbacks
Given a Studio Admin with Reconcile permission views a class roster within the past 30 days When they select an audit entry and choose Rollback Then the system creates a new audit entry with action_type=rollback, pre_value=current state, post_value=reverted state, links to the target by audit_entry_id, and the original entry remains intact Given a user without Reconcile permission When they attempt a rollback Then the action is blocked and an error message indicates insufficient permissions Given a rollback affects a payment gate or attendance-based credit When rollback completes Then dependent financial/credit actions are compensated (reverse or reissue) and each compensation is logged as separate audit entries referencing the rollback
KPI Computation and Display
Given a date range and class/studio filters When the KPI view loads Then it displays: no_show_rate = no_shows / booked, late_arrival_rate = late_check_ins / attended, avg_queue_time (seconds from scan to confirmation), error_rate = reconciled_changes / total_check_ins, each with numerator and denominator tooltips Given filters are changed When applied Then KPI values recalculate within 2 seconds and match counts derivable from underlying audit entries within ±1 Given a user hovers a KPI info icon When the tooltip opens Then it shows the exact formula and inclusion rules (e.g., late = check-in after class start + configurable grace period)
Audit Trail Export
Given a studio admin selects Export with current filters When the export is requested Then CSV and XLSX files are generated up to 500,000 rows with columns: audit_entry_id, event_time (studio TZ), event_time_utc, child, class, roster_id, action_type, scan_source, staff, device, location, pre_value, post_value, notes, payment_gate_status, override_flag, request_id Given export processing exceeds 10 seconds When the job continues server-side Then the user is notified in-app and the result is delivered by email with a secure link that expires in 7 days Given PII export controls When exporting without Export PII permission Then child full names are masked to last_initial and the mask status is indicated in the file header
Scheduled Attendance Reports to Stakeholders
Given a studio admin schedules a weekly Attendance Summary When the schedule is saved Then emails are sent at the studio’s timezone to specified recipients with an inline KPI snapshot (no_show_rate, late_arrival_rate, avg_queue_time, error_rate), attached CSV of the period’s audit entries (respecting permissions), and a link to the pre-filtered Audit Trail Given an email delivery fails When the failure is detected Then the system logs the failure and notifies the scheduling admin via in-app alert and email within 15 minutes Given a recipient opts out When they click unsubscribe in the email Then they are removed from future sends for that report and the preference change is recorded as an audit entry

Smart Grid Builder

Generate accurate seat maps in minutes using templates for reformer, cycling, mat, and row layouts. Drag-and-drop equipment, set aisles and mirrors, and auto-number rows so every room matches reality. Reduces setup time and prevents seating mistakes across locations.

Requirements

Template Layout Library
"As a studio owner, I want to start from proven layout templates so that I can build accurate seat maps in minutes without manual setup."
Description

Provide pre-built templates for reformer, cycling, mat, and rowing configurations. Users can start from a template matching their modality, with defaults for spacing, equipment dimensions, row and column patterns, and standard aisle placements. Templates are customizable and saveable as organization presets, accelerating map creation while ensuring consistency across locations.

Acceptance Criteria
Modality Templates Availability
Given I open the Smart Grid Builder template library When the library loads Then Reformer, Cycling, Mat, and Rowing templates are visible And each template displays a preview thumbnail and summary defaults (spacing, equipment dimensions, row/column pattern, aisle placement) And the library renders within 2 seconds on a 10 Mbps connection
Apply Template Defaults to Canvas
Given an empty room canvas and I select the Cycling template When I click Use Template Then the system places equipment according to the template's default spacing, equipment dimensions, row/column counts, and aisle placements And no equipment overlaps or extends beyond room boundaries And all items are aligned to the grid And the layout renders within 1.5 seconds for up to 100 items
Customize Template Parameters
Given a loaded template When I adjust spacing, equipment dimensions, row/column counts, or aisle placements via the properties panel Then the canvas updates to reflect changes within 200 ms of releasing the control And the system enforces modality-specific minimums to prevent overlap and unsafe spacing And I can reset any parameter to the template default in one action And an unsaved changes indicator is shown until I save or discard
Save as Organization Preset
Given a customized template When I save it as an Organization Preset with a unique name Then the preset stores spacing, equipment dimensions, row/column pattern, and aisle placements And name uniqueness is enforced within the organization namespace And the preset is available to all org locations within 5 seconds of save And the preset persists across sessions and is retrievable after logout/login
Apply Organization Preset Across Locations
Given a saved Organization Preset and a room with different dimensions When I apply the preset Then the system offers Fit to Room and Keep Counts options with a visual preview And the applied layout stays within room boundaries with no overlaps And aisle positions remain proportional to template definition when Fit to Room is chosen And equipment counts remain unchanged when Keep Counts is chosen; if the room cannot accommodate, the system warns and blocks apply
Template Consistency and Modality Validation
Given I attempt to save a preset When equipment types in the layout do not match the selected modality or required parameters are missing Then the system prevents save and lists specific validation errors And when all required parameters are valid, the save succeeds And the resulting preset is tagged with its modality and only appears under that modality in the library
Drag-and-Drop Placement with Grid Snapping
"As an instructor, I want intuitive drag-and-drop tools with snapping and alignment so that my room layout matches reality precisely and quickly."
Description

Offer an interactive canvas for placing equipment, aisles, mirrors, and labels via drag-and-drop. Include magnetic grid snapping, rotation increments, alignment guides, spacing equalization, bulk select and duplicate, undo and redo, zoom, and pan to enable rapid, precise layout construction with minimal errors.

Acceptance Criteria
Drag-and-Drop with Magnetic Grid Snapping
Given a visible grid and snapping enabled When the user drags any placeable object (equipment, aisle, mirror, label) onto the canvas Then the object's anchor snaps to the nearest grid intersection when within 12 px of it And on drop, the object is placed at that snapped coordinate with ≤1 px deviation at 100% zoom And holding Alt/Option while dragging disables snapping for that drag operation And snapping remains accurate within ≤1 px deviation at 50%, 100%, and 200% zoom levels
Rotation with Fixed Increments
Given an object is selected When the user rotates it using the rotate handle Then rotation snaps to 0°, 90°, 180°, and 270° when within a ±3° threshold And when the user holds Shift while rotating, the final angle is constrained to 15° increments And upon release, if grid snapping is enabled, the object's position re-snaps to the grid
Alignment Guides and Snap-to-Object
Given two or more objects are on the canvas When the user moves one object near another Then alignment guides appear when left, right, top, bottom edges or centers align within 8 px And releasing the object snaps it to the aligned position with ≤1 px positional error And guides hide immediately when the alignment threshold is no longer met
Equal Spacing Distribution
Given three or more objects are selected When the user invokes Distribute Horizontally Then the horizontal gaps between adjacent objects become equal within ±1 px while the outermost objects' x positions remain unchanged And when the user invokes Distribute Vertically Then the vertical gaps between adjacent objects become equal within ±1 px while the outermost objects' y positions remain unchanged
Bulk Select and Duplicate
Given multiple objects exist on the canvas When the user drag-selects with a marquee Then objects intersecting the marquee are added to the selection and others are not And when the user presses Ctrl/Cmd+D Then duplicates of all selected objects are created offset by 20 px right and 20 px down And duplicated objects retain type, size, rotation, and label properties and receive unique identifiers And if snapping is enabled, each duplicate lands on the nearest grid intersection
Undo and Redo History
Given a sequence of actions (move, rotate, duplicate, delete, align, distribute) has occurred When the user presses Ctrl/Cmd+Z repeatedly Then each action is undone in reverse order for at least the last 50 actions And when the user presses Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y Then the undone actions are redone in original order And zoom and pan actions do not affect the undo/redo history And the undo/redo stack persists for the current layout session until a new layout is created or an existing layout is loaded
Zoom and Pan Controls with Precision Snapping
Given the canvas is loaded When the user zooms via Ctrl/Cmd + mouse wheel or pinch gesture Then the zoom level changes smoothly in steps no larger than 10% per wheel tick within a range of 10% to 400% And when the user holds Spacebar and drags or uses the middle mouse button Then the canvas pans without moving selected objects And grid snapping and alignment behaviors remain accurate within ≤1 px at all supported zoom levels
Aisle, Mirror, and Obstruction Zones
"As a studio manager, I want to mark aisles and obstructions so that seats are not placed where they shouldn’t be and flow and safety are preserved."
Description

Enable definition of aisles, mirror walls, doors, and structural obstructions that constrain seat placement and numbering. Support non-bookable zones to prevent placing seats in blocked areas and maintain safe egress. Allow mirror orientation toggles to align left and right perspectives for modalities like cycling and reformer.

Acceptance Criteria
Prevent seat placement in non-bookable zones
Given a room grid with defined Aisle, Door, and Obstruction zones When the user drags or drops a seat into any non-bookable zone Then the seat is not placed, an inline error is shown, and the cursor snaps back to the nearest valid cell And the total seat count does not increase And Save and Publish are disabled until all invalid placements are resolved
Enforce configurable minimum aisle width
Given a configured minimum aisle width for the room When the user draws or resizes an Aisle narrower than the minimum Then a validation error is shown and the change is rejected And when equipment or seats are moved such that the aisle becomes narrower than the minimum Then the move is blocked and the aisle remains at or above the minimum width And on Publish, validation fails and lists any aisles violating the minimum width
Mirror orientation toggles left/right perspective
Given a mirror wall exists on one side of the room and the mirror orientation toggle is visible When the user switches the orientation from Left to Right (or Right to Left) Then the grid preview mirrors horizontally relative to the mirror wall And seat numbers and left/right labels flip to match the new perspective And non-bookable zones remain fixed relative to walls And the selected orientation is saved with the room and reflected in attendee-facing seat selection
Auto-numbering respects aisles and obstructions
Given rows with gaps caused by Aisle and Obstruction zones When Auto-number Rows is executed Then numbering skips non-bookable cells and remains contiguous within each row And the numbering direction follows the current mirror orientation setting And numbers are unique within the room with no duplicates And re-running auto-number after adding or removing seats updates numbers without breaking contiguity (except across non-bookable gaps)
Door egress clearance keeps exits unblocked
Given a Door zone with a configurable egress clearance area When the user attempts to place a seat within the clearance area Then placement is blocked with an explanatory message And when the door is moved, the clearance area moves with it and violations are re-validated in real time And on Save or Publish, the map cannot be marked valid if any seats violate door clearance
Zone visibility, selection, and locking
Given the Layers panel with toggles and Lock controls for Aisle, Mirror, Door, and Obstruction zones When a zone type is hidden Then its overlay is not shown but its constraints still apply to placement and numbering And when a zone type is locked Then the user cannot select, move, or resize those zones And on attendee-facing booking, zone overlays are not visible and only bookable seats are shown
Auto Numbering and Labeling Rules
"As a front desk staffer, I want seats auto-numbered to our convention so that bookings and in-room signage always align."
Description

Automatically generate row and seat labels with configurable sequences, starting corner, direction, and skip logic for aisles. Support prefixes and suffixes per modality, seat aliases, and visibility controls for staff-only or hidden seats to ensure booking labels match in-room placards and client communications.

Acceptance Criteria
Generate Labels with Configurable Row/Seat Sequences
Given a 3-row by 4-seat grid And row sequence is alphabetic starting at A And seat sequence is numeric starting at 1 And label format is {row}{seat} with no separator When labels are generated Then row 1 labels are A1, A2, A3, A4 And row 2 labels are B1, B2, B3, B4 And row 3 labels are C1, C2, C3, C4
Apply Starting Corner and Direction Rules
Given a 2-row by 3-seat grid And starting corner is bottom-right And horizontal direction is right-to-left And vertical progression is bottom-to-top And row sequence is numeric starting at 1 And seat sequence is numeric starting at 1 And label format is R{row}-S{seat} When labels are generated Then seat at (row 2, col 3) is labeled R1-S1 And seat at (row 2, col 2) is labeled R1-S2 And seat at (row 2, col 1) is labeled R1-S3 And seat at (row 1, col 3) is labeled R2-S1 And seat at (row 1, col 2) is labeled R2-S2 And seat at (row 1, col 1) is labeled R2-S3
Aisle Skip Logic Maintains Contiguous Numbering
Given a 2-row by 5-seat grid And column 3 is marked as an aisle And row sequence is alphabetic starting at A And seat sequence is numeric starting at 1 per row When labels are generated Then the aisle column has no label in any row And row A visible seats are labeled: col1=A1, col2=A2, col4=A3, col5=A4 And row B visible seats are labeled: col1=B1, col2=B2, col4=B3, col5=B4
Modality-Based Prefix and Suffix Application
Given modality is set to Rowing And label prefix is "Erg-" and label suffix is "-C" And row sequence is alphabetic starting at A And seat sequence is numeric starting at 1 And the grid has 1 row and 2 seats When labels are generated Then the seat labels are Erg-A1-C and Erg-A2-C
Seat Aliases for Client-Facing vs Staff Labels
Given modality Cycling defines a seat alias of "Bike" And internal staff label format is C{row}{seat} And a 1-row by 2-seat grid generates internal labels CA1 and CA2 When labels are presented in staff-facing views and exports Then staff see CA1 and CA2 When labels are presented in client-facing booking UI and communications Then clients see Bike 1 and Bike 2 And selecting Bike 2 books the same seat as internal label CA2
Visibility Controls for Staff-Only and Hidden Seats
Given a 1-row by 4-seat grid with labels A1, A2, A3, A4 And seat A2 is marked Staff-only And seat A3 is marked Hidden When a client views the public booking UI Then A2 and A3 are not visible or selectable And A1 and A4 are visible and selectable When a staff member views the staff booking UI Then A2 is visible and selectable to staff And A3 (Hidden) is not visible in staff UI And exported staff rosters include A2 but exclude A3
Room Dimensions and Scale Calibration
"As a studio owner, I want to set room dimensions and equipment sizes so that the seat map matches real space and avoids placement errors."
Description

Capture room length and width in feet or meters and apply equipment footprints to calibrate scale. Support wall offsets and minimum clearance rules, and provide real-time validators that flag overlaps or spacing below thresholds to ensure physically accurate layouts that mirror the real environment.

Acceptance Criteria
Enter Room Dimensions in Feet or Meters
Given I am in the Smart Grid Builder for a room When I set units to feet or meters and enter length and width with up to two decimal places Then the values are accepted, saved, and displayed to two decimal places Given I switch units between feet and meters When I confirm the change Then the numeric values convert using 1 ft = 0.3048 m with a maximum rounding error of ±0.01 in the displayed unit Given valid dimensions are entered When I reload the builder Then the dimensions and selected unit persist
Automatic Scale From Room Dimensions
Given I have entered room length and width When I click Apply Then the canvas scale updates so the room is rendered to scale, the aspect ratio matches the entered dimensions within 0.5%, and a scale indicator (px/ft or px/m) is displayed
Calibrate Scale Using Equipment Footprints
Given an equipment type with a known stored footprint is placed on the canvas When I apply Calibrate by Footprint Then the canvas scale updates so the equipment's rendered width and height match its stored footprint within ±1 px Given the scale has been calibrated When I check the on-canvas measurement between two points known to be 10.00 units apart in the selected unit Then the displayed measurement is 10.00 ± 0.02 in that unit
Configure Per-Wall Offsets
Given a rectangular room is defined When I set a left, right, top, or bottom wall offset value in the current unit Then a no-placement zone with the exact offset width is rendered along that wall and equipment cannot be placed within it Given wall offsets are configured When I save and reload the layout Then the offsets persist and continue to block placement and routing correctly
Define Minimum Clearance Rules by Equipment Type
Given minimum front, rear, and side clearances are defined for an equipment type When I place that equipment near a wall or another equipment Then clearance guides render and show green when clearance ≥ the rule and red when clearance < the rule Given any placement violates a clearance rule When I attempt to save the layout Then the save is blocked and an error list identifies each violation with equipment ID, location, required clearance, and actual clearance
Real-Time Overlap and Spacing Validation
Given two or more equipment items are on the canvas When I drag one item so that its footprint overlaps another Then an overlap error badge appears within 150 ms and on drop the move is rejected (snap-back) if overlap remains Given minimum spacing thresholds are configured When I drag items so spacing falls below the threshold Then a spacing warning appears within 150 ms and a distance indicator shows the shortfall in the current unit
Unit Switching Without Layout Drift
Given a layout with calibrated scale and placed equipment exists When I switch units between feet and meters Then all equipment positions and clearances remain physically equivalent with positional drift ≤ 1 px and all rule evaluations (overlap, spacing, clearance) yield identical Pass/Fail outcomes before and after the switch
Capacity Sync with Booking and Waitlist
"As an operations manager, I want seat maps to sync with class capacity so that changes instantly reflect in bookings and automate waitlist promotions."
Description

Synchronize seat counts and availability with class capacity so that adding or removing seats updates capacity in real time. Trigger existing waitlist-to-payment automations when seats are published, lock seats during edits to prevent double-booking, and validate before publish to ensure referential integrity.

Acceptance Criteria
Real-Time Capacity Update on Seat Change
Given a published class with a seat map and current capacity When an admin adds, removes, or toggles availability of seats and publishes the changes Then the class capacity equals the count of available reservable seats within 2 seconds in Admin UI and Public Booking UI And the Booking API reflects the new capacity and seat availability within 2 seconds And the system does not allow capacity to drop below the number of confirmed + held bookings
Waitlist-to-Payment Automation on Published Seats
Given a class has an active waitlist and new seats are published When capacity increases due to publishing seats Then the waitlist-to-payment automation triggers within 5 seconds in FIFO priority order And each invited customer receives a 10-minute hold on a seat, preventing others from booking it And on successful payment within the hold window, the booking is confirmed and capacity adjusts accordingly And on hold expiry or payment failure, the offer is revoked and the next waitlisted customer is invited until seats are filled or the waitlist is exhausted And no customer receives more than one concurrent offer for the same class
Edit Lock Prevents Double-Booking During Seat Map Changes
Given an admin opens the seat map editor for a class occurrence When the editor session starts Then an edit lock is placed on the occurrence’s seat inventory, preventing new bookings on affected seats And attempts to book a locked seat via UI or API return 409 Conflict with error code "SeatLocked" And only one active edit lock is allowed per occurrence; concurrent edit attempts receive 423 Locked And the lock auto-expires after 15 minutes of inactivity, is released on cancel/close, and is released on successful publish
Publish Validation Ensures Referential Integrity
Given draft seat map changes are ready to publish When the admin clicks Publish Then the system validates that capacity >= confirmed + held bookings And all booked/held seat IDs exist in the new map and remain available And all seat IDs in the map are unique and valid per numbering rules And all references from bookings, holds, and waitlist entries resolve to seats in the new map And if any validation fails, publish is blocked, no changes are applied, and a list of specific errors (with counts and IDs) is displayed And on success, changes are applied atomically, a new seat map version is created, and an audit entry (actor, timestamp, diff, affected IDs) is recorded
Capacity Sync Across Linked Sessions and Recurring Classes
Given a recurring class series with future occurrences When an admin edits the seat map and selects scope "This occurrence only" or "This and all future occurrences" Then capacities update only for the targeted occurrence(s) within 2 seconds and are reflected in UI and API And waitlist-to-payment automations trigger only for occurrences where seats were newly published And past occurrences are not modified And publishing across multiple targeted occurrences is transactional; if any one fails validation, none are published and errors identify the failing occurrences
Atomic Publish and Rollback on Downstream Failure
Given publishing includes capacity update, waitlist triggers, and edit lock release When a downstream step (e.g., notification service) fails after changes are applied Then the entire operation is rolled back to the pre-publish state and the edit lock remains active And the admin is notified of the failure with a correlation ID, and an error audit entry is recorded And no waitlist invitations are sent unless the capacity change is committed And retried publishes are idempotent, producing at most one capacity change and one set of notifications
Versioning, Approval, and Location Rollout
"As a multi-location admin, I want version control and controlled rollout so that I can maintain consistency while accommodating local needs."
Description

Provide draft and publish states, change logs, and rollback for map versions. Implement role-based permissions for editing and approval. Allow pushing approved templates to multiple locations with local overrides, ensuring consistency across studios while accommodating room-specific variations.

Acceptance Criteria
Draft and Publish States with Validation
Given an Editor creates or edits a seat map template, When they save, Then the template is stored as Draft with a unique draft ID and last-edited timestamp. Given a Draft template, When the Editor submits it for approval, Then its state changes to Pending Approval and it becomes read-only to Editors. Given a Pending Approval template with all required fields completed (layout type, capacity, seat numbering, aisles, mirrors), When an Approver publishes it, Then the state becomes Published and a sequential version number is assigned. Given a template missing required fields, When publish is attempted, Then publishing is blocked and specific validation errors list all missing or invalid fields.
Role-Based Permissions and Approval Workflow
Given system roles Editor, Approver, and Viewer, When an Editor attempts to publish, Then the action is denied (HTTP 403) and only "Submit for Approval" is available. Given an Editor submits a template for approval, When an Approver who did not author the last change reviews it, Then they must Approve or Reject with a mandatory comment. Given an Approver approves, Then the template transitions to Published and the Editor and Approver receive notifications; if Rejected, Then it returns to Draft with the rejection reason visible to the Editor. Given a user without edit permissions, When they attempt to modify a Published template, Then the action is blocked and the attempt is audit logged.
Change Log and Version History
Given any content change or state transition occurs, When the action completes, Then an immutable audit entry is recorded with user ID, role, timestamp (UTC), action type, previous state, new state, and a summary. Given two versions exist, When viewing differences, Then the system highlights added/removed/moved seats, renumbering, and aisle/mirror/config changes. Given the history view is requested, When exporting, Then CSV and JSON exports of the change log for a selected date range are available for download. Given a new version is published, Then the prior Published version remains read-only in history and selectable as a rollback target.
Rollback to Prior Published Version
Given at least two Published versions exist, When an Approver selects a prior version and confirms rollback, Then a new Published version is created that matches the selected version and becomes active. Given rollback completes, Then a change log entry is added with action "Rollback", source version ID, target version ID, and Approver ID. Given existing class bookings reference seats, When rollback occurs, Then seat identifiers are remapped to preserve bookings or a migration report lists any mapping exceptions; no bookings are deleted. Given the current Published version equals the selected target, When rollback is attempted, Then the action is blocked with an explanatory message.
Multi-Location Rollout with Scheduling and Idempotency
Given a template in Published state, When an Approver selects multiple locations and an effective date/time, Then a rollout job is queued with per-location statuses (Queued, In Progress, Succeeded, Failed). Given the rollout executes, When the effective time is reached, Then each selected location receives the template as a new Published version and non-selected locations remain unchanged. Given the same rollout (template version, locations, effective time) is triggered again, When executed, Then the operation is idempotent and does not create duplicate versions. Given a location fails to apply the rollout, When retries are enabled, Then the system retries up to a configured limit with exponential backoff and surfaces error details; manual retry is available.
Local Overrides Preservation on Global Updates
Given a location has local overrides for an inherited template, When a new global template version is rolled out, Then local overrides persist and only non-overridden fields update. Given a global change conflicts with a local override outside allowed bounds, When rollout occurs, Then the system flags a conflict, prevents auto-overwrite, and prompts the location manager to resolve. Given a rolled-out template at a location, When editing locally, Then only fields designated as locally overridable are editable and all other fields are read-only. Given a location with overrides, When viewing the template, Then an Overrides Summary lists overridden fields and their origin and is exportable.

PerfectFill

A policy-aware auto-fill engine that places clients for optimal vibe and capacity—front-to-back, checkerboard, or spread-out—based on your rules. Respects blocked seats, member priority, ADA holds, and buddy reservations so instructors don’t have to reshuffle. Maximizes utilization while keeping client preferences intact.

Requirements

Policy-Aware Seating Rule Engine
"As an instructor, I want seat placement to follow my studio’s rules automatically so that I don’t have to manually reshuffle the room before class."
Description

A deterministic engine that assigns clients to spots based on instructor-selected fill strategies (front-to-back, checkerboard, spread-out) and studio policies including member tier priority, ADA holds, blocked seats, and capacity constraints. Supports per-class and per-location rule sets, weighted tie-breakers (booking timestamp, membership status, attendance history), and configurable fallbacks when constraints conflict. Integrates with ClassTap’s booking and waitlist services to produce seat assignments that maximize utilization while honoring constraints, and persists assignments for downstream notifications and check-in.

Acceptance Criteria
Fill Strategy Application (Front-to-Back, Checkerboard, Spread-Out)
Given a class session with a defined seat layout and an active fill strategy (front-to-back, checkerboard, or spread-out) When the engine assigns seats to a set of confirmed bookings Then for front-to-back: seats are assigned in ascending seat_index defined by the layout And for checkerboard: no two assigned seats have Manhattan distance = 1 (edge-adjacent) And for spread-out: the minimum pairwise distance among assigned seats is maximal over feasible assignments And total assigned seats does not exceed capacity And the assignment is deterministic for identical inputs
Member Tier Priority Enforcement
Given mixed member tiers and limited available seats When the engine orders candidates for seat assignment Then all higher-tier members are seated before lower-tier members when capacity is constrained And clients within the same tier are ordered by the configured tie-breakers And non-members are only seated after all members with equal or better priority have been considered And an audit record of the priority resolution is stored per assignment
ADA Holds and Blocked Seats Respected
Given seats marked ADA-held with a release timestamp and seats marked blocked/unavailable When assigning seats before the ADA hold release Then only clients with ADA eligibility are assigned to ADA-held seats And no client is assigned to blocked/unavailable seats And after ADA hold release, ADA-held seats are treated per studio policy (general or continued hold) And zero assignments violate ADA holds or blocked status in any run
Buddy Reservations Seated Together
Given a booking with a buddy group size greater than 1 and a seat layout with proximity constraints When assigning seats for the group Then the group is assigned the required number of seats within the configured proximity (e.g., contiguous or same row/zone) And if insufficient qualifying seats exist, the engine applies the configured fallback (e.g., search alternate zone or leave group unassigned) without splitting the group And the assignment respects capacity, blocked seats, ADA holds, and member priority And the result is deterministic for identical inputs
Deterministic Conflict Resolution via Weighted Tie-Breakers and Fallbacks
Given two or more candidates eligible for the same seat(s) When a conflict occurs during assignment Then the engine orders candidates by the configured weighted tie-breakers (e.g., booking timestamp, membership status, attendance history) and selects the highest-ranked And if weights produce an exact tie, a deterministic secondary key (e.g., client_id ascending) resolves it And when no feasible assignment exists under current constraints, the engine executes the configured fallback chain in order and records the step used And repeated runs with identical inputs yield identical assignments and audit trails
Waitlist Auto-Assignment with Payment Capture
Given a class with a waitlist and payment required on promotion When a seat becomes available Then the highest-priority waitlisted client per policy is atomically promoted and assigned a seat within 5 seconds And payment authorization/capture is initiated; on failure, the promotion is rolled back and the next eligible client is attempted And race conditions are prevented so that no more than one client is assigned to the same seat And all transitions (promotion, payment success/failure) are recorded with timestamps
Assignment Persistence and Downstream Notifications
Given seats have been assigned When the assignment transaction commits Then seat assignments are persisted with versioning and idempotency keys And the booking API returns the assigned seat_id(s) on subsequent reads And assignment events are emitted to notification and check-in services at least once with deduplication keys And instructor and client notifications reflect the final seat within 10 seconds
Seat Map Management & Layout Editor
"As a studio owner, I want to define accurate room layouts and special seats so that PerfectFill can place clients in the right spots every time."
Description

Tools to create and manage room layouts with labeled rows/columns, zones, and spot metadata (ADA, blocked, instructor view, mirrors). Supports templates per class type, versioning, and layout activation schedules. Allows import/export (CSV/JSON) and visual editing, with validation to ensure seat counts match class capacity. Exposes layout data to the rule engine and check-in UI, and maintains compatibility with mobile and desktop instructor dashboards.

Acceptance Criteria
Create and Save Layout with Labeled Rows, Columns, and Zones
Given I open the Layout Editor for a new room When I add 4 rows (A–D) and 10 columns (1–10), define zones Front/Middle/Back with explicit boundaries, and save as "Studio A 40" Then the layout saves with exactly 40 uniquely labeled seats (A-1…D-10), no duplicates or nulls And the preview renders row/column headers and zone boundaries as defined And the layout is listed in the Library with seatCount=40, zones=3, and lastModified within 5 seconds of save
Seat Metadata Flags and Business Rules Enforcement
Given I select seats and apply metadata: ADA (A-1,A-2), Blocked (B-1), Mirror (C-3), InstructorView (D-4) When I save the layout Then ADA, Blocked, Mirror, and InstructorView flags persist on those seats And blocked seats are excluded from auto-assignment and capacity calculations And ADA seats remain countable toward capacity but are prioritized for ADA-eligible members by PerfectFill And seat detail UI shows all assigned metadata for each seat
Templates per Class Type with Versioning and Activation
Given a class type "Hot Yoga 60" When I create template version v1 and set it Active immediately Then exactly one version is Active for that class type When I create v2 and schedule activation for next Monday 00:00 local Then v1 remains Active until that time, after which v2 becomes Active and v1 is Archived And past sessions retain their originally referenced version; no retroactive changes
Capacity Validation on Assignment
Given a layout with 28 total seats (of which 2 are blocked) and a class session with capacity 26 When I assign the layout to the session Then validation succeeds because (total seats − blocked seats) equals class capacity (28−2=26) Given a layout where assignable seats ≠ class capacity When I attempt assignment Then the system blocks assignment and shows an error explaining the mismatch and options to Edit layout or Adjust capacity And the assignment only succeeds when assignable seats equal class capacity
Import/Export Layouts (CSV/JSON) with Round-Trip Fidelity
Given a valid CSV with headers seatId,row,column,zone,flags and 24 rows When I import the file Then the system validates schema, rejects duplicates, and creates 24 uniquely labeled seats with matching metadata And invalid files yield a non-blocking report with line numbers, field names, and error codes; no partial saves occur When I export the layout to JSON and re-import it Then the resulting layout matches field-for-field (semantic equality) with the original And imports accept UTF-8 CSVs with CRLF or LF; exports are UTF-8; files up to 10 MB are accepted
Expose Layout Data to Rule Engine and Check-In UI
Given a class session has an assigned layout When PerfectFill requests layout data Then the API returns seat positions, zones, and metadata within 300 ms p95 for up to 200 seats And the Check-in UI renders the seat map with correct symbols (ADA, Blocked, Mirror, InstructorView) on mobile and desktop And seat selection responds within 100 ms p95; mobile tap targets are at least 44x44 px And assignments made by PerfectFill are reflected in the UI within 1 second of API response
Layout Activation Scheduling and Conflict Handling
Given versions v1 and v2 of a template for class type "Spin 45" When I schedule v2 to activate on 2025-10-01 06:00 studio local time Then sessions starting at or after that timestamp use v2; earlier sessions use v1 And if overlapping schedules are created, the most recent explicit activation takes precedence and a warning is shown And at any instant there is at most one Active version per class type And scheduling respects the studio time zone and DST transitions; no session is left without an assigned layout
Real-time Auto-Fill Triggers
"As a client, I want my seat to be assigned instantly when I book or get off the waitlist so that I know exactly where I’m sitting."
Description

Event-driven processing that runs PerfectFill on booking, cancellation, waitlist promotion, hold release, and payment confirmation. Ensures atomic seat assignment and prevents double-bookings under concurrency. Provides instant feedback in the booking UI, updates attendee rosters, and sends seat confirmation notifications. Includes rate limiting and debouncing to batch rapid changes without sacrificing responsiveness.

Acceptance Criteria
Auto-Fill on New Booking Submission
Given a class instance has available capacity and PerfectFill rules are configured And a client submits a valid booking request via the booking UI When the booking request is accepted by the API Then PerfectFill is triggered within 200 ms of request validation And a seat is assigned according to policy (front-to-back, checkerboard, or spread-out) while respecting blocked seats, member priority, ADA holds, and buddy reservations And the seat assignment is recorded atomically with a single seat_id and state=reserved (pending payment) And the booking UI receives seat assignment feedback within 300 ms And the attendee roster reflects the reserved seat state within 1 second
Auto-Fill on Cancellation
Given an attendee cancels a confirmed or reserved seat in a class instance When the cancellation is confirmed by the system Then PerfectFill is triggered within 200 ms And the canceled seat is released and PerfectFill rebalances seating according to policy without violating client preferences or blocked seats And if waitlist auto-promotion is enabled, the top-eligible waitlisted client is considered for immediate assignment And the attendee roster updates within 1 second to reflect changes And affected clients receive seat release/assignment notifications within 2 seconds
Auto-Fill on Waitlist Promotion
Given a class instance has at least one waitlisted client and a seat becomes available When promotion rules select the next eligible waitlisted client Then PerfectFill is triggered within 200 ms And a seat is assigned that respects blocked seats, member priority, ADA holds, and buddy reservations And the promoted client’s booking status updates and the booking UI reflects the assigned seat within 300 ms And a seat confirmation notification including seat_id is sent within 2 seconds And no other client is concurrently assigned the same seat_id
Auto-Fill on Hold Release
Given a class instance contains seats under time- or policy-based holds (e.g., ADA or instructor blocks) And one or more holds reach their scheduled release time When the hold(s) transition to released Then PerfectFill is triggered within 200 ms And previously ADA-reserved seats are not assigned to non-eligible clients unless ADA-release policy explicitly allows it And seats are assigned per policy and priority while maintaining buddy reservations and respecting remaining holds And the attendee roster updates within 1 second and notifications dispatch within 2 seconds
Auto-Fill on Payment Confirmation
Given a booking with a reserved seat is pending payment When the payment is confirmed successfully Then the seat assignment is committed atomically to the booking and marked confirmed And no other booking can acquire the same seat_id And if payment fails or times out, the reserved seat is released and PerfectFill re-runs to rebalance without assigning the seat to the failed booking And the booking UI reflects the confirmed or released state within 300 ms And a seat confirmation (or failure) notification is sent within 2 seconds
Concurrency and Atomic Seat Assignment
Given N (>=2) clients simultaneously attempt to book the last K (>=1) seats of the same class within a 100 ms window When all booking requests are processed Then at most K bookings are persisted with distinct seat_ids and state in {reserved, confirmed} And all other requests receive a deterministic seat_unavailable response within 500 ms and are not charged And no two bookings reference the same seat_id at any time And data stores show no oversell across retries or restarts
Rate Limiting and Debounced Batching
Given multiple seat-affecting events (bookings, cancellations, promotions, hold releases, payment confirmations) occur rapidly for the same class instance When events arrive within a 250 ms debounce window Then the system coalesces them into a single PerfectFill execution for that class instance And no more than 10 PerfectFill executions per class instance per second are allowed (rate limit) And end-to-end latency from the first event in the batch to applied seat updates does not exceed 500 ms under normal load And the booking UI receives a single consolidated update reflecting all changes And all individual events in the window are reflected in the final seat state (no drops)
Preferences, Buddy Linking, and Proximity Rules
"As a client, I want to sit with my friend and near my preferred area so that my class experience feels comfortable and consistent."
Description

Support for client placement preferences (e.g., front row, aisle, near mirror), saved per profile and adjustable per booking, with opt-in privacy controls. Buddy reservations allow grouping multiple attendees to adjacent or same-zone spots, with intelligent relaxation of preferences when perfect adjacency is impossible. Honors ADA and blocked seats, member priority, and class-specific restrictions while balancing utilization and satisfaction.

Acceptance Criteria
Save and Booking-Level Override of Placement Preferences
Given a client profile with saved preferences: front row, aisle, near mirror And the client’s privacy setting is Public When the client books Class A and overrides preferences to back row and near mirror only Then the booking record for Class A stores the override values And the auto-placement for Class A uses the booking-level overrides And the client’s profile preferences remain unchanged And a subsequent booking for Class B uses the unchanged profile preferences unless overridden again And removing the override on Class A restores use of the profile preferences for any re-run of placement
Preference Visibility Privacy Controls
Given a client’s preferences are marked Private When an instructor views the roster or seat map for the client’s class Then no specific preference details are displayed, only a “Private” indicator And the auto-placement engine uses the private preferences for placement And an audit event “preferences_used_without_exposure” is logged for the booking And if the client toggles privacy to Public before a placement re-run, the specific preferences become visible to instructors on re-run
Buddy Linking Places Adjacent or Same-Zone
Given a group of N buddies with adjacency requested and sufficient eligible seats exist When auto-placement runs Then the engine assigns the buddies to N contiguous seats in the same row if such a block exists within constraints And if no such block exists, the engine assigns all buddies within the same zone with at most a 1-seat gap between any two group members And the booking succeeds only if all group members can be placed under these rules; otherwise the group remains unplaced or is waitlisted as a unit
Intelligent Preference Relaxation Order
Given exact adjacency or seat preferences cannot be met for a booking or buddy group When auto-placement runs Then relaxation is applied strictly in this order: adjacency -> aisle/mirror -> row -> zone And ADA holds and blocked seats are never relaxed or violated And the maximum separation allowed after relaxation is 2 seats within the same zone; beyond that the group is not placed unless “Allow split group” is enabled on the booking And any relaxed placements are flagged “Relaxed” in the instructor UI and in the booking activity log
ADA Holds, Blocked Seats, and Member Priority Enforcement
Given seats marked ADA with a release time and seats marked Blocked When auto-placement runs Then non-ADA clients are never placed into ADA seats before the ADA release time And blocked seats are never assigned under any circumstance And after the ADA release time, unclaimed ADA seats become eligible for general placement And when placing both members and non-members, members are placed first; ties are resolved by booking timestamp; already-placed clients are not displaced
Class-Specific Placement Restrictions Enforced
Given class rules exist (e.g., front row reserved for Advanced; newcomers restricted to back two rows) When an auto-placement or manual move would violate a class rule Then the action is prevented and the user is shown the specific rule causing the block And auto-placement never produces a placement that violates any active class rule
Utilization and Preference Satisfaction Optimization
Given a class with capacity C and M bookings with preferences and buddy links When auto-placement runs Then the selected placement maximizes TotalScore = 0.6*UtilizationRate + 0.4*AvgPreferenceSatisfaction And UtilizationRate = placed_seats / min(C, M) And AvgPreferenceSatisfaction per client is computed as: adjacency/buddy satisfied (0.4), row/zone preference satisfied (0.4), aisle or mirror satisfied (0.2); each component scored 1 if satisfied, 0 if not; averaged across placed clients And the chosen placement’s TotalScore is within 2% of the optimal score produced by the reference solver on the same inputs And no mandatory constraints (ADA, blocked, class rules) are violated to achieve optimization
Instructor Override, Locks, and Audit Trail
"As an instructor, I want to tweak seat assignments with clear warnings and locks so that I can handle edge cases without breaking policy."
Description

Interactive drag-and-drop reassignment in the roster view with real-time rule validation, soft/hard constraint warnings, and the ability to lock placements for VIPs or ADA needs. Provides undo/redo, change history with who/when/why, and safe previews before publishing changes. Ensures downstream systems (notifications, check-in, attendance) stay in sync after manual adjustments.

Acceptance Criteria
Drag-and-Drop Reassignment with Real-Time Rule Validation
Given the roster view is open and seats are visible with their statuses When the instructor drags an attendee from Seat A to Seat B Then rule evaluation runs and renders validation feedback within 200 ms of hover-over and on drop And if no hard constraints are violated, the attendee appears in Seat B immediately in the UI And an unsaved changes indicator is displayed within 100 ms And no notifications or downstream updates are sent while in draft state
Soft vs Hard Constraint Handling and Messaging
Given rule sets include blocked seats, ADA holds, member priority, and vibe/preference rules When a proposed drop violates a hard constraint (e.g., blocked seat, ADA hold without eligibility) Then the drop is rejected, Seat A remains assigned, and a red inline message specifies the rule name and seat within 200 ms When a proposed drop violates only soft constraints (e.g., vibe pattern, preference) Then the drop is allowed after a single-click confirmation on a yellow banner that enumerates the soft rules impacted And the confirmation action is required only once per move And if the user has the "Override Hard Constraints" permission and enters a reason of at least 10 characters Then a hard constraint drop can proceed and is flagged as an override in history
Seat Locking for VIP/ADA and Enforcement
Given an attendee in the roster When the instructor applies a seat lock Then a lock icon appears on the seat and row within 100 ms And PerfectFill and any auto-arrange actions cannot move the locked assignment And other users without "Unlock Seats" permission cannot move or unlock it When a move is attempted on a locked seat by an unauthorized user Then the action is blocked and an informational tooltip explains the lock and owner When a lock is removed by an authorized user Then a reason of at least 10 characters is required and the change is recorded in history
Buddy Reservations and Group Moves
Given two attendees are linked as buddies with adjacency preference When the instructor drags one buddy to a new seat Then the system proposes a paired move to maintain adjacency and highlights candidate seats And if adequate adjacent seats are unavailable, a modal presents options: pick alternative section, split with reason, or cancel And selecting split requires "Allow Buddy Split" permission and a reason of at least 10 characters And ADA holds and blocked seats are respected during suggested placements
Undo/Redo Stack and Draft State
Given the instructor has made manual placement changes in the current session When the instructor presses Undo Then the most recent change is reverted within 150 ms, including seat assignment, locks, and any warnings state And up to the last 20 placement operations are undoable in order When the instructor presses Redo without making a new change Then the reverted change is reapplied within 150 ms And after Publish, the undo/redo stack is cleared And undo/redo actions do not trigger notifications or downstream sync until Publish
Audit Trail: Who/When/Why with Export
Given manual changes are made in the roster Then each change is recorded with timestamp (UTC), user ID, user display name, before/after seat IDs, impacted rules, whether override occurred, and optional reason And the history panel lists changes in reverse chronological order and can be filtered by user and date range And exporting history produces a CSV with the same fields and is available within 3 seconds for up to 5,000 records And published records are immutable and include a publish event summarizing the net diff (moved/locked/unlocked counts)
Preview and Publish with Downstream Sync
Given there are draft changes When the instructor opens Preview Then a summary shows affected attendees, rule impacts, locks, and pending notifications without sending messages When the instructor clicks Publish and confirms Then the assignments persist atomically and all downstream systems are updated And attendee notifications are queued within 60 seconds according to notification settings And check-in kiosks and attendance views reflect the new roster within 10 seconds of publish And no duplicate notifications are sent for attendees whose seat did not change And if any downstream update fails, the publish is rolled back and the user sees an error with a retry option
Concurrency, Conflict Resolution, and Idempotency
"As a platform admin, I want seat assignments to be correct under heavy load so that clients never see double-booked or lost seats."
Description

A transactional workflow that handles simultaneous bookings and modifications with optimistic locking, idempotency keys, and deterministic tie-breakers. Implements retry queues for transient failures and guarantees exactly-once seat assignment. Detects and resolves conflicts arising from last-minute cancellations, no-shows, and overrides without disrupting the class roster.

Acceptance Criteria
Simultaneous Last Seat Booking
Given a class with capacity N and available seats = 1 and PerfectFill rules active And two booking requests R1 and R2 arrive within the same 100 ms window with unique idempotency keys When both transactions attempt to reserve the last seat Then a deterministic tie-breaker prioritizes by membership_tier DESC, created_at ASC, request_id ASC And only the winning transaction commits with seat assignment and version increment by 1 And the losing transaction receives 409 Conflict with a waitlist offer token valid for 15 minutes And the final roster shows exactly one new attendee and no double-booked seat And an audit event records tie-breaker inputs and winner within 200 ms end-to-end
Idempotent Retry After Timeout
Given a booking request with idempotency_key K and payload P And the initial client response times out after the server persisted the outcome When the client retries with the same K and identical payload P within 24 hours Then the system returns the original outcome (success, failure, or processing) without creating any additional seat assignment or payment charge And duplicate payment intents and seat records remain at count <= 1 for the transaction And if a retry uses the same K with a different payload P', the system returns 422 Idempotency Key Reuse with no side effects And all requests with key K are retrievable for at least 7 days for traceability
Instructor Override During Checkout
Given a customer at checkout holds a provisional seat S assigned by PerfectFill And an instructor concurrently blocks S or moves a buddy reservation affecting S When the booking transaction attempts to commit Then optimistic locking detects a seat-map version change and triggers re-evaluation And the engine reassigns the customer to the best valid seat without displacing seated clients outside policy And if no valid seat exists, the transaction aborts with 409 Conflict and presents alternative classes/times And at no time is any seat double-assigned and the roster remains consistent after commit
Waitlist Auto-Promotion Race
Given multiple waitlisted clients W1..Wn with priority factors (membership, buddy grouping, join_time) And a seat opens due to cancellation within T minutes of class start When auto-promotion runs while manual bookings may also occur Then exactly one candidate is selected using order: membership DESC, buddy grouping intact, join_time ASC And payment is attempted atomically; transient failures (5xx, timeouts, deadlocks) are retried with exponential backoff up to 5 attempts And on payment success, the candidate moves to confirmed with exactly one seat assignment; others remain waitlisted And on permanent failure, the next candidate is processed; no candidate is charged or seated more than once And each promotion attempt is guarded by a unique idempotency key per candidate
No-Show Release at Check-in Cutoff
Given no-shows are identified at cutoff T minutes before class start And their seats are released while concurrent kiosk and mobile bookings occur When PerfectFill reallocates newly available seats Then seat-map updates serialize via optimistic locking with no lost updates And exactly-once assignment holds across channels; no duplicate attendees or seats occur And seated clients are not moved unless policy allows; ADA holds and preferences are preserved And roster and notifications reflect changes within 2 seconds without disrupting instructor view
Transient Failure Retry with Exactly-Once Guarantees
Given booking, cancellation, and seat-move operations are executed as transactional jobs And an operation encounters a transient failure (DB deadlock, gateway 502, broker NAK) When the retry queue processes the job Then retries use exponential backoff (200ms, 400ms, 800ms, 1600ms, 3200ms) up to 5 attempts And idempotency keys enforce at most one committed state change and at most one successful charge And metrics capture attempt_count and final_status; alerts fire if attempt_count exceeds threshold And partial states are rolled back or compensated; the roster is left consistent after each attempt
PerfectFill API and Webhooks
"As a tech-savvy studio owner, I want to sync seat assignments to my signage and messaging tools so that clients see up-to-date placements everywhere."
Description

REST/GraphQL endpoints and webhooks to retrieve and subscribe to seat assignments, changes, and rule evaluation results. Enables integrations with studio displays, CRM, email/SMS, and partner apps. Includes pagination, filtering by class/session, auth via API keys/OAuth, and versioning. Emits events such as seat_assigned, seat_changed, and rule_violation_warning for external automations.

Acceptance Criteria
Retrieve Seat Assignments with Pagination and Filtering
Given a valid API client and class_session_id When the client calls GET /v1/seat-assignments?class_session_id={id}&limit=50 Then the response status is 200 and returns up to 50 seat assignments for that session And each item includes seat_id, seat_label, client_id, status (assigned|held|blocked), assigned_at (ISO 8601), and rule_applied_ids (array) And the response includes pagination.next_cursor when more results exist When the client calls GET /v1/seat-assignments?class_session_id={id}&cursor={next_cursor} Then the next page is returned without duplicate items from the previous page When the client adds filters status=assigned and updated_since={timestamp} Then only matching records are returned When class_session_id is invalid or not found Then the response status is 404 with error.code="not_found" When the client performs an equivalent GraphQL query seatAssignments(sessionId:, first:, after:, status:, updatedSince:) Then the fields and pagination behavior mirror REST
Subscribe to Webhooks and Receive seat_assigned Events
Given a valid subscription request with https endpoint, secret, and event_types ["seat_assigned"] When the client calls POST /v1/webhooks/subscriptions Then the response status is 201 and returns subscription_id and event_types configured When PerfectFill assigns a seat in the subscribed account Then the system sends a POST to the subscribed endpoint with payload containing id, event_type="seat_assigned", occurred_at (ISO 8601), version, class_session_id, seat_id, client_id, source="perfectfill", and metadata.correlation_id And the request includes header X-CT-Signature with sha256 HMAC signature of the raw body using the shared secret When the receiver replies with any 2xx status Then the event is acknowledged and not retried When the receiver replies with non-2xx or times out Then the event is retried with exponential backoff up to 8 attempts When retries occur Then all deliveries for the same event share the same id to support idempotency
Authenticate via API Key and OAuth 2.0
Given Authorization: Bearer {api_key} with scope read:seat_assignments When the client calls GET /v1/seat-assignments Then the response status is 200 Given Authorization: Bearer {oauth_access_token} with scopes read:seat_assignments write:webhooks When the client calls POST /v1/webhooks/subscriptions Then the request is authorized and returns 201 When credentials are missing, expired, or invalid Then the response status is 401 with error.code="unauthorized" When the token or key lacks the required scope for the endpoint Then the response status is 403 with error.code="insufficient_scope" and a WWW-Authenticate header indicating required scopes When an API key is rotated and the old key is past its revoke_at Then requests with the old key return 401
Versioned Endpoints and Backward Compatibility
Given a request to GET /v1/seat-assignments without Accept-Version header When processed Then the v1 response schema is returned When the client requests GET /v2/seat-assignments or sets header Accept-Version: v2 Then the v2 schema is returned Within a major version, fields may be added but existing fields are not removed or changed in type or semantics When a deprecated version reaches its published sunset_date Then requests to that version return 410 with error.code="version_sunset" and include deprecation headers sent for at least 90 days prior All webhook payloads include a top-level version field matching the emitting API version
Retrieve Rule Evaluation Results for a Session
Given a valid API client and class_session_id When the client calls GET /v1/rule-evaluations?class_session_id={id} Then the response status is 200 and each item includes rule_id, rule_name, outcome (applied|skipped|violated), reason, affected_seat_ids, affected_client_ids, and evaluation_at (ISO 8601) When the client calls GET /v1/rule-evaluations?assignment_id={id} Then only evaluations associated with that assignment are returned When the client adds include=trace Then the response includes an ordered steps array showing rule evaluation order and priority with step numbers When the client executes a GraphQL query ruleEvaluations(sessionId:, assignmentId:, includeTrace:) Then the fields and filtering mirror REST
Webhook Delivery Reliability and Security
All webhook subscriptions must use https URLs unless the account is in sandbox mode Every webhook request includes X-CT-Signature header with sha256= signature computed via HMAC-SHA256 over the raw body using the subscription secret Events are acknowledged only on 2xx responses; non-2xx or timeouts trigger retries with exponential backoff up to 8 attempts Duplicate deliveries for the same event use the same id to enable idempotent processing Event delivery order is preserved per class_session_id for a given endpoint; a newer event for the same session is not delivered until the prior one is acknowledged or retried to exhaustion
Seat Change and Rule Violation Warning Notifications
When a client's seat assignment changes within a session Then a seat_changed webhook is emitted with payload including id, event_type="seat_changed", class_session_id, client_id, seat_before_id, seat_after_id, reason_code, changed_at (ISO 8601), and version When PerfectFill detects a potential rule conflict during assignment Then a rule_violation_warning webhook is emitted with payload including id, event_type="rule_violation_warning", class_session_id, rule_id, rule_name, severity (warning|error), description, affected_seat_ids, affected_client_ids, occurred_at (ISO 8601), and version For both event types, delivery, signature, retry, and ordering behaviors match those defined for seat_assigned events Subscriptions can be limited to specific event_types at creation, and only the selected event_types are delivered

HealthLock

Seat-level maintenance controls let you mark equipment as Out of Service or Needs Attention. The optimizer excludes those spots, rebalances the map, and logs a task for follow-up, preventing last-minute scramble and protecting client safety and trust.

Requirements

Seat Status Tagging & Metadata Capture
"As a studio manager, I want to mark an equipment seat as Out of Service with notes and photos so that staff and the system prevent bookings and know what to fix."
Description

Enable per-seat status controls with standardized states (Operational, Needs Attention, Out of Service) accessible from class maps, inventory views, and mobile. Capture structured metadata including reason codes, freeform notes, photos/videos, timestamp, staff ID, and optional effective and auto-expiry times. Support bulk updates, validation against active bookings, and propagation to all upcoming classes that reference the affected seat. Persist status across sessions, reflect changes in real-time UI with clear visual badges and tooltips, and maintain cross-location asset identity for studios with multiple rooms or branches.

Acceptance Criteria
Single Seat Status Update from Class Map (Desktop)
Given a published class map is open on desktop and I have manage permissions When I select seat S and choose status "Needs Attention," select a reason code, and add a note Then seat S displays a "Needs Attention" badge and a tooltip showing status, reason code label, note, timestamp, and my staff ID And the status persists after page refresh and user relogin And seat S becomes ineligible for new bookings in that class and any other upcoming classes referencing seat S And an audit log entry is recorded with before/after state and full metadata
Bulk Seat Status Update from Inventory View (Desktop)
Given I am on the inventory view and multi-select 10 seats across rooms When I apply status "Out of Service" with reason code "Mechanical" and set auto-expiry for a specific date/time Then seats without active bookings update successfully; seats with active bookings are listed as conflicts and are not updated And the UI shows per-seat success/failure with counts and downloadable details And for successful seats, the status and metadata propagate to all upcoming classes that reference those seats And stored metadata includes reason code, freeform note, timestamp, staff ID, and effective/expiry times for each updated seat
Mobile Quick Tagging with Media Upload
Given I open the mobile app and navigate to a class map or scan the asset QR to open seat S When I set status to "Out of Service," select a reason code, attach up to 3 photos and 1 video, and submit Then the media uploads successfully (supported types: jpg, png, mp4; max total size 100 MB) and is associated to the status entry And the status, media, timestamp, and my staff ID are saved and visible on both mobile and desktop within 5 seconds And a media thumbnail gallery appears in the seat S details/tooltip for quick review
Status Effective and Auto-Expiry Scheduling
Given I schedule seat S to "Needs Attention" effective at 18:00 today with auto-expiry at 09:00 tomorrow When the system time reaches 18:00 today Then seat S automatically transitions to "Needs Attention" and becomes ineligible for new bookings in all upcoming classes And when the system time reaches 09:00 tomorrow, seat S automatically returns to "Operational" unless a newer status was applied after scheduling And both transitions are recorded in the audit log with timestamps and system actor
Active Booking Validation on Status Change
Given seat S has 2 active bookings in upcoming classes When I attempt to set seat S to "Out of Service" effective immediately Then a conflict dialog lists impacted classes and attendees, and the change is blocked until I choose a resolution And if I choose "Reassign to available seat," eligible seats are proposed; upon confirmation, bookings are moved and the status change is applied And if I cancel, no changes are saved And all actions are logged with actor, timestamp, and before/after seat assignments
Cross-Location Asset Identity and Propagation
Given seat S has a global asset ID and is scheduled in Room A today and Room B tomorrow When I set S to "Needs Attention" from the studio-level inventory view Then the status and metadata apply to S regardless of room assignment and propagate to all upcoming classes across rooms/branches referencing S within 10 seconds And all views display a consistent asset ID, status badge, and tooltip content And moving S between rooms does not create a new asset record or lose its status history
Real-time UI Broadcast and Visual Indicators
Given multiple users have the same class map or inventory view open on web and mobile When one user changes the status of seat S and saves Then all other sessions reflect the change in under 2 seconds without manual refresh And seat S displays state-specific visual badges (Operational: none, Needs Attention: amber wrench, Out of Service: red X) and a tooltip containing status, reason code label, note (first 140 chars), media thumbnails, effective/expiry, timestamp, and staff ID And screen readers announce the status change with accessible labels And the change persists across page reloads and sign-ins
Real-time Optimizer Exclusion & Rebalancing
"As an instructor, I want flagged seats to be excluded and attendees re-seated automatically so that I avoid last-minute scrambling and preserve a smooth class layout."
Description

Automatically exclude flagged seats from availability and trigger the seating optimizer to rebalance class layouts while preserving constraints such as ADA accessibility, buddy/group preferences, instructor-defined formations, and capacity limits. For existing bookings, perform intelligent reseating to the nearest equivalent seat and, if none are available, initiate a manual resolution flow. Ensure waitlist auto-promotion respects updated capacity and constraints. Deliver sub-200ms response for seat-state changes, guarantee idempotency and rollback on failure, and emit events for downstream consumers.

Acceptance Criteria
Seat Flagging Excludes Availability and Rebalances Layout <200ms
Given a class with an available seating map and an operator flags a seat as Out of Service or Needs Attention with a valid idempotency key When the seat-state change request is processed Then the flagged seat is removed from availability across API and cache instantly and the optimizer returns a new layout version excluding the seat within 200ms P95 end-to-end latency for the request-response cycle And the response includes layoutVersion, excludedSeatIds, correlationId, and effectiveAt timestamps And no new bookings can be created for the flagged seat after the response time
Constraint-Preserving Rebalance
Given a class layout with ADA seats, buddy/grouped attendees, instructor-defined formations/roles, and a capacity limit When one or more seats are flagged and a rebalance is triggered Then the resulting layout preserves all constraints: ADA attendees remain in ADA-compliant seats, grouped attendees remain adjacent as defined, formation roles are maintained, and total assigned seats do not exceed capacity And the optimizer reports zero constraint violations in its result payload And any seat that cannot be reassigned without violating constraints is marked for manual resolution
Intelligent Reseating of Existing Bookings
Given an attendee is assigned to a seat that becomes flagged When the rebalance runs Then the attendee is automatically moved to the nearest equivalent seat as defined by same zone/tier, same formation role, and minimal Manhattan distance from the original seat And tie-breakers are resolved deterministically by lowest seatId And the attendee and instructor receive a reseating notification with oldSeatId, newSeatId, and reason And the reassignment occurs without cancelling or duplicating the booking
Manual Resolution Flow When No Equivalent Seat Available
Given an attendee sits in a seat that is flagged and no equivalent seat is available without violating constraints When the rebalance completes Then a manual resolution task is created with classId, attendeeId, originalSeatId, suggestedOptions[], and SLA dueAt And the attendee’s booking remains active with seat status set to PendingAssignment and is excluded from auto-check-in And the admin UI shows a blocking banner for the class until all pending assignments are resolved
Waitlist Auto-Promotion Honors Updated Capacity and Constraints
Given a class has a waitlist and seats are flagged causing a rebalance When capacity and constraints after rebalance allow additional attendees Then waitlisted attendees are auto-promoted in FIFO order within 5 seconds, only if they can be seated without violating constraints And promotions that cannot satisfy constraints are skipped (without losing waitlist position) and retried on subsequent changes And promoted attendees are assigned seats and charged according to policy, with audit logs capturing waitlistId -> bookingId linkage
Idempotent Seat-State Changes
Given a client submits a seat-state change with Idempotency-Key K for classId C and seatId S When the same request is retried with the same key K within 24 hours Then the API returns 200 with the original response body, no duplicate optimizer runs are scheduled, and no duplicate events or tasks are created And conflicting idempotency keys for the same natural key (C,S,changeAt) are rejected with 409 and no state change occurs
Transactional Rollback and Event Emission
Given a seat-state change triggers a rebalance and downstream effects When any step fails (optimizer error, constraint solver timeout, event bus publish failure, cache write failure) Then all changes are rolled back to the previous layoutVersion and availability state, and the API responds with an error including correlationId And a HealthLock.SeatStateChangeFailed event is emitted with reason, classId, seatId, previousLayoutVersion, attemptedLayoutVersion, and correlationId And on success, a HealthLock.SeatStateChanged event is emitted within 100ms of commit with CloudEvents-compliant envelope and at-least-once delivery semantics
Maintenance Task Auto-Creation & Workflow
"As a facilities lead, I want a task created whenever equipment is flagged so that issues are tracked to resolution without manual follow-up."
Description

On status change to Needs Attention or Out of Service, auto-create a linked maintenance task with prefilled asset, location, severity, attachments, and SLAs. Assign to role or user, set due dates, and provide checklists, comments, and status transitions (Open, In Progress, Blocked, Resolved, Verified). Send notifications to assignees, allow escalations and recurring inspections, and integrate with calendars and external tools via webhooks/API. Maintain a complete audit trail and ensure bidirectional linking between the task and the seat for traceability.

Acceptance Criteria
Auto-Create Task on Seat Status Change
Given a seat associated with an asset and location is marked Available When a user or API changes the seat status to "Needs Attention" or "Out of Service" and saves Then the system auto-creates a maintenance task with status "Open" linked to that seat And the task is prefilled with asset, location, and severity according to configured mapping for the selected status And any attachments included at the time of status change are copied into the task And SLA timers for the task are initialized according to the severity's SLA policy
Auto-Assignment and Due Date from SLA
Given assignment rules exist mapping severity and/or location to a role or user When a maintenance task is created Then the system assigns the task to the configured role or user And the task due date is set based on the SLA for its severity, honoring configured business hours and timezone And the assignee and due date are visible on the task detail view and in list views
Workflow Transitions, Checklists, and Comments
Given a maintenance task exists with an active checklist When an authorized user updates the task status Then the system only allows transitions among Open, In Progress, Blocked, Resolved, and Verified as defined by the workflow And the status change is blocked if required checklist items are not complete when moving to "Resolved" or "Verified" And users can add comments at any status, with each comment recording author and timestamp And all checklist item state changes (complete/incomplete) are tracked with user and timestamp
Notifications and Escalations
Given notifications are enabled for maintenance tasks When a task is created, assigned, status changed, due-soon per SLA threshold, or overdue Then the assignee receives a notification via configured channels And if the task becomes overdue, an escalation notification is sent to the configured escalation target And notifications include task ID, linked seat, current status, and due date/time
Recurring Inspections and Calendar Integration
Given a recurring inspection schedule is configured for a location or asset When the schedule triggers according to its cadence Then the system creates the next set of maintenance tasks linked to the relevant seats or assets And each task with a due date creates or updates a corresponding calendar entry in connected calendars And calendar entries update or cancel when the task due date changes or the task is set to Resolved or Verified
Webhooks and API for External Tools
Given external integrations subscribe to maintenance task webhooks When a task is created, updated, status changed, escalated, or verified Then the system emits a webhook event containing task ID, seat ID, asset/location identifiers, action type, timestamps, actor, and changed fields And the webhook is signed for verification and retried on transient failure And an authenticated API endpoint is available to fetch tasks by ID and to filter by seat, status, or severity
Bidirectional Linking and Audit Trail
Given a maintenance task is linked to a seat When viewing either the seat details or the task details Then each view displays a link to the other with identifiers and current status And the system maintains an immutable audit trail of all task field changes, checklist updates, comments, and status transitions with user and timestamp And audit entries are viewable in the UI and retrievable via the API
Client Communication & Transparency
"As an attendee, I want to be informed if my seat changes due to maintenance so that I’m not surprised when I arrive and can adjust."
Description

Notify affected clients when their seat is changed or when class capacity is adjusted due to maintenance, using brandable templates across email, SMS, and push. Include clear explanations, the new seat assignment, and options to manage preferences or switch classes when applicable. Show in-app badges indicating maintenance in progress without exposing sensitive details. Provide localization, rate limiting, and delivery tracking, and log all communications for reference.

Acceptance Criteria
Seat Change Trigger & Recipient Selection
Given a seat is marked Out of Service or Needs Attention and a client’s seat assignment changes, When the update is saved, Then only the impacted clients are flagged for notification with a unique correlation ID per client-event. Given class capacity is reduced due to maintenance, When seats are removed, Then all booked clients who lose their original seat or are moved/waitlisted are flagged for notification, and unaffected clients are excluded. Given multiple maintenance edits occur within 2 minutes for the same class, When notifications are prepared, Then the system coalesces them into a single notification per client per class. Given a client has multiple bookings in the same class block, When processing notifications, Then the client receives at most one notification per class occurrence.
Brandable Multi-Channel Notification Content
Given an impacted client is flagged, When a notification is generated, Then the message includes: (a) brief maintenance explanation, (b) new seat label/row/number or status (moved/waitlisted), (c) class name, date/time, location, (d) links to Switch Class and Manage Preferences, and (e) support contact. Given an organization has branding configured, When the message is rendered for email/SMS/push, Then the org logo, colors, and sender name are applied and pass template validation with no placeholder errors. Given a template is used, When dynamic fields (seat, date, location, client name) are merged, Then all placeholders resolve and the rendered preview matches expected values for a test booking. Given email content is rendered, When viewed in common clients, Then the logo has alt text and the layout is responsive to mobile widths (≤ 375px).
Channel Selection, Delivery SLA, and Rate Limiting
Given a client’s channel preferences exist, When sending notifications, Then messages are sent only via opted-in channels in priority order: Push, SMS, Email; if no preferences exist, default to Email only. Given an impacted client is flagged, When the notification is sent, Then at least one channel is dispatched within 2 minutes of the seat/capacity change and any fallback channel within 5 minutes if the preferred channel fails. Given repeated maintenance events for the same class within a short interval, When dispatching notifications, Then rate limiting enforces: max 1 notification per client per class per 5 minutes and max 3 maintenance notifications per client per rolling 24 hours across all classes. Given provider-level errors occur, When retries are attempted, Then exponential backoff is used up to 3 attempts per channel before falling back to the next available channel.
In-App Maintenance Badge Visibility and Privacy
Given a class has seats marked Out of Service or Needs Attention, When a client views the class detail or seat map, Then a "Maintenance" badge is displayed within 5 seconds and no specific equipment IDs or sensitive details are shown. Given a client uses assistive technology, When the badge is focused or read by a screen reader, Then it announces "Maintenance in progress; some seats unavailable" and meets color contrast ratio ≥ 4.5:1. Given the maintenance status is resolved, When the class detail is refreshed, Then the badge is removed within 60 seconds and affected seat availability updates accordingly. Given keyboard-only navigation, When hovering or focusing the badge, Then a tooltip is accessible via keyboard and dismissible with Escape.
Localization and Timezone Formatting
Given a client has a language preference, When generating notifications and in-app text, Then the localized template is used and UI strings are displayed in the client’s language; if unavailable, fall back to org default, then English. Given a client has a timezone, When dates/times are rendered, Then they are displayed in the client’s timezone with locale-appropriate formatting (e.g., 24h vs 12h). Given a right-to-left language is used, When rendering the notification and badge, Then layout direction is RTL and punctuation/numerals follow locale rules. Given pluralizable strings (e.g., seats), When content is generated, Then correct plural forms are applied per locale rules.
Delivery Tracking, Retries, and Audit Logging
Given a notification is dispatched, When provider callbacks or send attempts occur, Then delivery status is tracked as Queued, Sent, Delivered, or Failed with timestamps and provider message IDs. Given any notification attempt completes, When writing to the audit log, Then the log contains: client ID, class ID, event type, channel, correlation ID, template ID + version, rendered subject preview (first 100 chars), status, and actor (system/automation). Given a delivery fails, When retry policy is applied, Then retries follow 1m, 5m, 15m backoff; after exhaustion, status is Failed and an admin-visible alert is created. Given an admin views a client’s communication history, When filtering by event type = Maintenance, Then all related entries are listed within 2 seconds and exportable to CSV. Given data retention policy, When older records are evaluated, Then communication logs are retained for at least 12 months.
Preferences Management and Class Switching
Given a client receives a notification, When the Manage Preferences link is used, Then the client can opt in/out per channel for maintenance notifications and changes are saved immediately and respected on subsequent sends. Given a client wants to change classes, When the Switch Class link is used, Then the client is taken to a pre-filtered list of compatible alternatives for the same pass with real-time availability and can confirm switch within 3 clicks. Given a client confirms a switch, When the operation succeeds, Then the original booking is canceled without penalty, the new booking is created, and a confirmation is sent; if it fails, the original booking remains and the client is informed. Given SMS is used, When the client replies STOP, Then SMS is unsubscribed for non-essential messages and the system returns a confirmation; transactional seat-change SMS continue only if required and permitted, else fallback to Email.
Role-Based Access & Approvals
"As an owner, I want only authorized staff to mark seats out of service or back online so that safety and accountability are maintained."
Description

Implement permissions that restrict who can change seat status, edit or close maintenance tasks, and override optimizer decisions. Support roles such as Owner, Manager, Instructor, Front Desk, and Maintenance, configurable per location. Offer optional approval for returning equipment to service, including approver identity, timestamp, and notes. Enforce authorization checks in UI and API, and present read-only indicators to unauthorized users.

Acceptance Criteria
Seat Status Change Authorization (UI and API)
Given Location A grants "Seat Status: Modify" to Owner and Maintenance only When a Front Desk user attempts in the UI to mark Bike 12 Out of Service Then the control is disabled, no state change occurs, and a tooltip "Insufficient permissions" is displayed Given the same permissions at Location A When a Front Desk user calls POST /locations/A/seats/12/status with { status: "out_of_service" } Then the response is 403 with error_code "RBAC_FORBIDDEN" and the seat status remains unchanged Given a Maintenance user at Location A with "Seat Status: Modify" When they mark Bike 12 Out of Service via UI or API Then the status updates to Out of Service and the change is reflected on the seat map within 1 second
Maintenance Task Edit and Close Authorization
Given Location A grants "Task: Edit" and "Task: Close" to Owner, Manager, and Maintenance only When an Instructor opens a maintenance task in the UI Then all task fields are read-only and the Close action is hidden or disabled Given the same permissions When an Instructor calls PATCH /tasks/{id} with any changes or status "Closed" Then the response is 403 with error_code "RBAC_FORBIDDEN" and no task fields are changed Given a Maintenance user with the required permissions When they edit task fields and save Then changes persist and the task updated_at timestamp is refreshed Given a Maintenance user with "Task: Close" When they close a task Then a non-empty resolution_reason is required and the task status becomes "Closed" with closed_at timestamp recorded
Optimizer Override Authorization and Indicators
Given Location A grants "Optimizer Override" to Owner and Manager only When an Instructor attempts to include an excluded seat or manually reassign a seat that conflicts with optimizer decisions Then the UI shows read-only state for override controls and the API returns 403 with error_code "RBAC_FORBIDDEN" Given a Manager at Location A with "Optimizer Override" When they apply an override to include Seat 7 in the class map Then the seating plan updates and Seat 7 displays an "Override" badge visible to all users Given any user without "Optimizer Override" When they view a plan with an override applied Then they can see the "Override" badge and details but cannot modify or remove the override
Location-Specific Role Configuration Enforcement
Given User U has role Maintenance at Location A and no roles at Location B When U attempts to change seat status at Location B Then the UI renders controls disabled and the API returns 403 with error_code "RBAC_FORBIDDEN" Given an Owner assigns Maintenance to User U at Location B When U refreshes or re-authenticates within 30 seconds Then U can change seat status at Location B without requiring application restart Given resources are scoped by location When a user has permission at Location A only Then the same action against a Location B resource is denied regardless of global role settings
Approval Workflow for Return to Service
Given Location A has "Return to Service: Approval Required" enabled and approver roles set to Owner and Manager When a Maintenance user submits Bike 12 to return to service via UI or API Then a pending approval record is created, Bike 12 remains Out of Service, and the API responds 202 with an approval_request_id Given a pending approval for Bike 12 When an Owner approves and provides non-empty notes Then Bike 12 status changes to Available and the approval record stores approver identity, UTC timestamp, and notes Given a pending approval for Bike 12 When an Instructor attempts to approve or reject via UI or API Then the action is blocked in UI and the API returns 403 with error_code "RBAC_FORBIDDEN" Given Location A disables "Return to Service: Approval Required" When a Maintenance user sets Bike 12 to Available Then the status updates immediately without creating an approval record
Unauthorized Read-Only Indicators and Accessibility
Given a user lacks permission for seat status changes, task edit/close, or optimizer overrides When they view the relevant UI components Then action controls render disabled with a lock icon and tooltip "Read-only: insufficient permissions" Given the same user and UI When they attempt to interact via click or keyboard Then no network request is sent and focus remains on the control with aria-disabled="true" for accessibility Given the user inspects details of seats, tasks, or overrides When permissions are lacking Then read-only badges are shown and no destructive actions (Save, Close, Override) are visible
Reporting, Metrics & Audit
"As a studio manager, I want visibility into maintenance trends and their business impact so that I can prioritize repairs and investments."
Description

Provide dashboards and exports showing seat downtime, mean time to repair, issue frequency by asset, impact on capacity and revenue, no-show correlation, and SLA adherence. Include filters by location, instructor, class type, and date range. Maintain immutable audit logs for all seat status changes, reseating events, optimizer actions, and client communications with timestamps and actor IDs. Support CSV export and read-only API endpoints for BI tools.

Acceptance Criteria
Ops reviews downtime, MTTR, and issue frequency dashboard
Given historical HealthLock events exist across multiple assets, classes, and locations within the selected date range When the user opens Reporting > HealthLock dashboard for that date range Then the widgets display Seat Downtime (minutes), Mean Time to Repair (hours), Issue Frequency by Asset (count), Capacity Impact (seat-minutes removed), Revenue Impact (currency), No-Show Correlation summary, and SLA Adherence (%) And each widget shows a total, per-asset breakdown, and a daily trend for the selected period And metric values match a known validation dataset within ±0.1% (or exact for counts) And the dashboard renders in under 3 seconds at the 90th percentile for up to 100,000 events
Analyst filters metrics by location, instructor, class type, and date range
Given an organization with data spanning multiple locations, instructors, class types, and dates When the analyst applies any combination of the Location, Instructor, Class Type, and Date Range filters Then all dashboard widgets and counts reflect the filters consistently And multi-select is supported for all filters and defaults to "All" And filters persist in the URL and are restored on reload and when shared And the organization time zone is used by default and can be switched to UTC, updating all charts and exports accordingly
CSV export of metrics and audit logs for external analysis
Given a filtered dashboard view is active When the user selects Export CSV > Metrics Summary Then a CSV file is generated within 30 seconds containing one row per asset-day with columns: date (ISO 8601), location_id, asset_id, asset_label, instructor_id, class_type, downtime_minutes, mttr_hours, issue_count, capacity_impact_seat_minutes, revenue_impact_amount, sla_met_flag And the file reflects the applied filters and totals match the dashboard within ±0.1% When the user selects Export CSV > Event Audit Log Then a CSV file is generated with one row per audit event with columns: event_id, event_type, timestamp_utc, actor_id, actor_type, location_id, class_id, asset_id, seat_id, from_status, to_status, reason_code, optimizer_action_id, client_comm_id, request_id And all timestamps are UTC ISO 8601, numeric fields are unformatted, column headers are consistent and documented, and files under 50 MB download immediately, otherwise large exports are queued with a downloadable link when ready
Read-only BI API access to metrics and audit logs
Given a valid API key with read:healthlock scope When the BI tool performs GET /api/v1/healthlock/metrics with date_range, location_id, instructor_id, and class_type filters Then the API returns 200 with a paginated JSON array including fields: date_utc, location_id, asset_id, downtime_minutes, mttr_hours, issue_count, capacity_impact_seat_minutes, revenue_impact_amount, sla_met_flag And results are ordered by date_utc asc then asset_id, and support pagination via cursor and limit (max 1000) When the BI tool performs GET /api/v1/healthlock/audit-logs with filters and pagination Then the API returns 200 with immutable event records and fields matching the Event Audit Log CSV And attempts to POST, PUT, PATCH, or DELETE on these endpoints return 405 Method Not Allowed And unauthorized or insufficient scope returns 401/403 with no data leakage
Immutable audit log for seat status changes and reseating
Given a user marks a seat Needs Attention or Out of Service, the optimizer reseats an attendee, or a client communication is sent When the action is committed Then an audit event is written with event_id, event_type, timestamp_utc, actor_id (user or system), location_id, class_id, asset_id, seat_id, from_status, to_status, reason_code, related_ids (optimizer_action_id or client_comm_id), and request_id And audit events are append-only; no update or delete operations are available in UI or API to alter existing records And any correction results in a new audit event linked to the prior via prior_event_id And events are retrievable within 5 seconds of the originating action and are ordered strictly by timestamp_utc then event_id
No-show correlation to equipment issues
Given historical class attendance and HealthLock event data exist When the user opens the No-Show Correlation widget for a selected date range and filters Then the widget displays two no-show rates: classes with ≥1 equipment issue in the 24 hours preceding class start and classes with none, plus the absolute and relative delta And the calculation uses confirmed no-shows divided by total booked seats per class, aggregated over the period And values match a known validation dataset within ±0.1 percentage points and update immediately when filters change
SLA adherence calculation and reporting
Given SLA thresholds are configured for MTTR (hours) and maximum downtime per asset per calendar month (minutes) When the user views SLA Adherence for a date range Then the dashboard displays percent of incidents meeting MTTR, percent of assets within downtime limits, count of breaches, and a list of breached assets with first_seen and resolved_at timestamps And SLA pass/fail status per incident and per asset is available in CSV export and API And calculations align to the organization time zone for period boundaries and match a validation dataset within ±0.1%

BuddyLink

Allow clients to reserve adjacent seats for a friend with a temporary hold link that auto-expires. Keeps groups together, increases conversions, and reduces manual seat swaps—while still protecting inventory with timed releases if the friend doesn’t confirm.

Requirements

One-Tap Buddy Hold Link
"As a client booking a class, I want to generate a temporary link to hold a seat for my friend so that we can book together without losing availability."
Description

Enable clients to reserve one or more adjacent seats for a friend by generating a unique, signed, single-use link that holds inventory for a configurable duration (e.g., 5–20 minutes). The system creates a hold at booking time via the reservation service, displays a visible countdown, and prevents double-booking across web, widget, and mobile apps. Hold parameters (duration, max buddy seats, per-user/day limits) are configurable at studio, location, or class level. The link deep-links to the BuddyLink landing screen, is compatible with logged-in and guest flows, and supports localization/branding. API endpoints and webhooks expose create/cancel/expire events for integration with invoices and attendee management. Audit logs capture all hold lifecycle events for compliance and support.

Acceptance Criteria
Signed Single-Use BuddyLink Generates and Holds Adjacent Seats
Given a client has booked their own seat(s) for a class with at least N adjacent seats available And studio configuration defines max buddy seats M and hold duration D minutes When the client taps "Hold seats for a friend" and requests N buddy seats (N ≤ M) Then the system creates a hold via the reservation service for N contiguous adjacent seats to the client’s seat(s) And generates a unique, signed, single-use BuddyLink bound to hold_id And the hold is persisted with status=Held-Buddy, started_at=now, expires_at=now + D And if N adjacent seats are unavailable, no hold is created and the user sees "Not enough adjacent seats available" And after a successful confirmation, the BuddyLink is invalidated and cannot be reused
Visible Countdown and Timed Auto-Release
Given a valid BuddyLink exists with an expiry timestamp T When the inviter or friend opens the booking UI (web, widget, or mobile) Then a countdown timer displays remaining time based on server time until T And if the timer reaches zero before confirmation, the held seats are released within 5 seconds and become available to others And the UI shows "Hold expired" and disables the confirm action And if the inviter cancels the hold before T, the timer stops and seats are released immediately with a confirmation message
Cross-Channel Double-Booking Prevention and Concurrency Control
Given seats are in Held-Buddy state for a hold_id When any other user attempts to select those seats on web app, embedded widget, or mobile app Then the system prevents selection and displays "Seat held" And if multiple devices open the same BuddyLink concurrently, only the first successful confirmation converts the hold; others receive "Link already used or expired" And seat allocation and confirmation are atomic and idempotent under concurrent requests
Configurable Hold Duration and Limits by Scope
Given studio-level defaults for duration D (5–20 minutes), max buddy seats M, and per-user/day holds limit L And optional overrides exist at location or class level When an admin sets and saves an override at a given scope Then new holds created in that scope use the override values, otherwise inherit from the nearest parent scope And requests exceeding M buddy seats are blocked with "Maximum buddy seats is M" And when a user reaches L holds in a rolling 24-hour window, further holds are blocked with "Hold limit reached" And attempts to set duration outside 5–20 minutes are rejected with validation errors
Deep-Link Landing Supports Auth States, Localization, and Branding
Given a BuddyLink is opened When the opener is logged in Then the BuddyLink landing shows class details, number of held seats, price summary, and a confirm CTA with account details prefilled When the opener is a guest Then the landing presents guest checkout requiring name, email, and payment method, and allows account creation or continuation as guest And all text is localized to the opener’s locale (or studio default) and the UI reflects studio branding (logo, colors, typography) And upon successful payment, the hold converts to a reservation and the attendee is added
API Endpoints and Webhooks for Hold Lifecycle
Given API credentials with reservations:holds scope When calling POST /holds with class_id, seats=N, and scope parameters Then the API returns 201 with hold_id, seats, expires_at, status=Held-Buddy, and link_url (signed) When calling POST /holds/{hold_id}/cancel Then the API returns 200 and seats are released And webhooks are emitted for hold.created, hold.canceled, and hold.expired with signed HMAC headers, include hold_id, class_id, seats, expires_at, and are retried on non-2xx with exponential backoff And POST endpoints support idempotency keys to prevent duplicate holds
Audit Logging of Hold Lifecycle Events
Given any hold lifecycle event occurs (created, link_opened, confirmed, canceled, expired) When the event is processed Then an immutable audit record is written with timestamp (UTC), actor (user_id or guest), channel (web/widget/mobile/API), IP, hold_id, and outcome And admins can query and export audit logs by date range, class_id, and user_id And audit records are read-only and retained per policy
BuddyLink Claim and Checkout Flow
"As a friend receiving a BuddyLink, I want a fast checkout that lets me claim the held seat and pay with my preferred method so that I can confirm before the hold expires."
Description

Provide a frictionless claim experience where the invitee opens the BuddyLink, sees class details, seat(s) on hold, price breakdown, and a countdown timer, then confirms via login, sign-up, or compliant guest checkout. Support payments via saved cards, Apple Pay/Google Pay, passes/memberships, and promo codes with tax/fee calculation. On success, convert the hold to a confirmed booking, attach the attendee to the class roster, and generate receipts/invoices. Handle edge cases: insufficient funds, expired link, already-booked user, capacity changes, and duplicate clicks with idempotency. Optionally allow the host to pay for the buddy (configurable). Emit analytics and conversion events, and update both parties in real time.

Acceptance Criteria
BuddyLink Open Shows Class Details, Hold Seats, and Countdown
Given a valid, unexpired BuddyLink with N seats held When the invitee opens the link Then the page displays class title, date/time, location, instructor, and remaining capacity And the held seats count N and their adjacency indicator are shown And a price breakdown (subtotal, taxes, fees, discounts) and total are displayed And a countdown timer shows server-side remaining hold time in mm:ss and decrements in real time When the invitee refreshes or opens the link on another device/browser Then the remaining hold time is consistent with the server and does not reset or create additional holds When the countdown reaches 00:00 without confirmation Then held seats are released, the link becomes unclaimable, and the call-to-action changes to Book Normally (if seats available) or Join Waitlist
Authenticate via Login, Sign-Up, or Compliant Guest Checkout
Given the invitee opens a valid BuddyLink When they proceed to confirm Then they can choose Login, Sign Up, or Continue as Guest When Login is selected and credentials are valid Then the user's profile and saved payment options are loaded When Sign Up is selected Then mandatory fields (first name, last name, email, password) are validated, unique email enforced, and account created before checkout continues When Continue as Guest is selected Then mandatory fields (first name, last name, email) and consent to Terms and Privacy are required and validated; no account is created Then the checkout cannot proceed until all required fields are valid and consent is recorded with timestamp and IP
Pay with Saved Card or Apple Pay/Google Pay
Given the invitee is eligible to self-pay When they view payment methods Then saved cards (if logged in) and Apple Pay/Google Pay are offered when supported by device/browser and merchant configuration When a saved card is selected Then taxes and fees are calculated, SCA/3DS is triggered when required, and on authorization/capture success a booking is created When Apple Pay/Google Pay is selected Then Payment Request is initialized with the order total and line items, and on success the booking is created When a payment is declined or insufficient funds occur Then an inline error explains the failure, no booking is created, no funds are captured, the hold remains until expiry, and the user can retry or switch methods
Use Pass/Membership or Apply Promo Code
Given the invitee has an applicable pass or membership When selected as tender Then the order total is reduced per plan rules and no external payment method is required if balance covers total Then pass punches or membership entitlements are decremented upon booking success and are not decremented on failure When a promo code is applied Then eligibility, usage limits, and expiration are validated before applying; the price breakdown updates and taxes/fees are recalculated correctly Then the total never becomes negative and discounts apply before taxes where required by locale configuration
Host Pays for Buddy (Configurable)
Given the host created a BuddyLink with Host Pays enabled When the invitee confirms Then the invitee owes $0 and sees Paid by Host messaging; their profile details are still required and validated Then the host is charged using the configured method at confirmation time; on success the invitee booking is created and the roster is updated When the host charge fails Then the invitee sees a clear error, may switch to self-pay without leaving the flow, and the hold persists until expiry
Expired, Capacity Change, or Already Booked Handling
When the invitee opens an expired or invalid BuddyLink Then an expiration/invalid message is displayed; if seats are available for normal purchase, a Book button is offered; otherwise, a Join Waitlist option is shown When class capacity is reduced during a valid hold Then existing held seats remain claimable until expiry; if the class is canceled or capacity is zero before confirmation, then the hold is voided and a cancellation message is shown When the invitee already has a booking for the class Then the system prevents duplicate booking, shows an Already booked message, releases the hold, and offers to view the existing booking
Idempotent Checkout, Duplicate Click Protection, and Real-Time Updates
Given the invitee is at the payment step When Confirm/Pay is clicked multiple times or the page is refreshed Then exactly one charge attempt and one booking record occur, with subsequent attempts returning the first result Upon booking success Then roster entries for both host and invitee update in real time for both parties within 2 seconds and the held seats convert to confirmed seats Then analytics events are emitted for buddylink_opened, buddylink_checkout_started, buddylink_payment_succeeded, buddylink_payment_failed, and buddylink_expired with properties (link_id, class_id, host_id, invitee_id or guest_id, amount, currency, payment_method, device, timestamp) Then receipts and invoices are generated and sent to relevant parties within 1 minute of success; none are sent on failure
Adjacent Seat Allocation Engine
"As an operations manager, I want the system to automatically assign adjacent seats and handle edge cases so that groups stay together and staff avoid manual seat swaps."
Description

Automatically allocate and hold contiguous seats next to the host for seated classes using the venue’s seating chart. If perfect adjacency isn’t available, choose the nearest viable cluster based on studio-defined rules (e.g., same row preferred, minimize distance). Respect accessibility and restricted seats, and disallow holds that violate accessibility policies. For open-floor classes without assigned seating, reserve capacity slots instead. Provide deterministic, thread-safe allocation under concurrency and expose seat suggestions to the UI before hold confirmation. Support reallocation if better adjacency becomes available before claim, without displacing confirmed bookings.

Acceptance Criteria
Allocate Contiguous Seats Next to Host (Seated)
Given a seated class with a seating chart and a host with confirmed seat H and a BuddyLink request for N buddy seats When a contiguous block of size N exists adjacent to H in the same row and all seats in the block are available (not held/confirmed/restricted/accessibility-designated) Then the engine selects the block immediately adjacent to H in the same row And creates a hold for those N seats tied to the BuddyLink with an expiry timestamp per studio setting And the held seats are excluded from other allocations until claim or expiry And repeating the request with identical inputs returns the same seat IDs (deterministic)
Nearest Viable Cluster When Perfect Adjacency Unavailable
Given no contiguous block of size N exists adjacent to host seat H in the same row and a studio rule set R is configured (prefer same row, minimize sum of seat-to-H distances; if tie, choose the lowest row index then the lowest starting seat index) When the engine computes adjacency candidates for a BuddyLink of N seats Then it selects the nearest viable cluster of size N that satisfies R and excludes restricted/accessibility/occupied/held seats And the top suggestion returned to the UI matches the cluster determined by R And the selection is deterministic for identical inputs
Accessibility and Restricted Seat Compliance
Given accessibility-designated seats and restricted seats exist on the seating chart When a BuddyLink allocation or suggestion is requested Then the engine shall not allocate or suggest accessibility-designated or restricted seats And if no compliant allocation exists without using such seats, the engine returns a "No compliant adjacent seats available" result without creating a hold
Open-Floor Class Capacity Reservation
Given a class is configured as open-floor (no assigned seating) with capacity C and current confirmed+held count U When a BuddyLink is created for N buddy spots Then the engine reserves min(N, C − U) capacity slots tied to the BuddyLink and reports failure if C − U < N And no seat IDs are returned; only capacity slots are held with expiry And capacity counts reflect the hold until claim or expiry
Concurrency-Safe Deterministic Allocation
Given multiple BuddyLink allocation requests are processed concurrently for the same class When they target overlapping seat regions Then no seat ID is assigned to more than one active hold or booking at any time And results are deterministic under identical inputs with stable tie-breaking (e.g., earlier request timestamp then BuddyLink ID) And each request is idempotent when retried with the same requestId (no duplicate holds created)
Pre-Confirmation Seat Suggestions to UI
Given a host requests suggestions for N buddy seats for a seated class When the engine evaluates the seating chart Then it returns an ordered list of candidate contiguous blocks that satisfy adjacency rules and exclude restricted/accessibility/occupied seats And no seats are held at the suggestion stage And each suggestion includes seat IDs and metadata needed for UI display (row, indices, distance score) and a TTL after which suggestions may be recalculated
Non-Disruptive Reallocation Before Claim
Given an active BuddyLink hold for N seats block B and the buddy has not yet claimed When a better block B' that more closely satisfies the adjacency rules becomes available Then the engine reallocates the hold from B to B' without displacing any confirmed bookings And the BuddyLink identifier remains unchanged and the hold expiry is not extended by reallocation And the UI/API reflects the new seat IDs and releases B immediately
Expiry, Auto-Release, and Conflict Resolution
"As a studio owner, I want held seats to auto-release and prevent conflicting claims so that inventory remains accurate and we avoid double-bookings."
Description

Enforce hold expiration with precise timers and atomic release of seats back to the inventory pool when the countdown ends or the host cancels. Invalidate links after use or expiry, display clear status messaging, and prevent oversells through idempotent claim endpoints and optimistic locking. Handle concurrency scenarios (simultaneous claims, capacity changes, or host cancellations) with deterministic outcomes and user-safe error states. Broadcast real-time updates to all affected clients via WebSocket or SSE so the host sees when a friend claims or when a hold expires. Persist complete lifecycle events for reconciliation and customer support.

Acceptance Criteria
Countdown Expiry and Atomic Seat Release
Given a host creates a BuddyLink hold for N seats with a server-side expiry T When the countdown reaches zero or the host cancels the hold before T Then all held seats are released back to the inventory atomically in a single transaction And the link state is set to EXPIRED or CANCELLED and is no longer claimable And the server-enforced timer accuracy is within ±1 second and the UI never accepts claims after server expiry And inventory counts are immediately accurate and non-negative across all channels And API responses include a machine-readable state (EXPIRED or CANCELLED) and an ISO-8601 timestamp
Idempotent Claim Endpoint Prevents Oversell
Given a friend submits a claim using a valid BuddyLink and the client may retry due to network issues When the claim endpoint is called multiple times with the same link token and idempotency key/request_id Then exactly one reservation is created and subsequent calls return the same reservation reference without creating additional seats And no more than N seats from the hold can be claimed and class capacity never goes below zero And concurrent general checkout cannot allocate seats currently held by the BuddyLink until they are released or claimed
Optimistic Locking for Simultaneous Claims
Given two or more clients attempt to claim the last available seat(s) at the same time When transactions are processed with optimistic locking/version checks Then only one transaction commits successfully and others fail with deterministic error codes (e.g., SOLD_OUT, HOLD_EXPIRED) And failed attempts leave no partial reservations, no orphan holds, and no negative inventory And the successful reservation is visible to all clients consistently after commit
Deterministic Outcomes Under Host Cancel or Capacity Change During Active Claim
Given a friend is on the checkout page using a BuddyLink and either the host cancels the hold or the class capacity is reduced When the friend submits the claim after the change Then if the hold was cancelled the claim is rejected with HOLD_CANCELLED and no payment is captured or reservation created And if capacity was reduced below available general seats, existing holds remain valid until expiry but new claims via that link are rejected with CAPACITY_REDUCED once no seats remain And in all cases inventory remains consistent and non-negative with no partial state
Real-Time Broadcasts to Affected Clients
Given the host roster view and the friend checkout are connected via WebSocket or SSE with a last-event-id When a hold is created, claimed, expired, cancelled, or capacity changes Then both host and friend receive the corresponding event payload (HOLD_CREATED, FRIEND_CLAIMED, HOLD_EXPIRED, HOLD_CANCELLED, CAPACITY_CHANGED) within 1 second of commit And UIs update roster counts, seat availability, and statuses without page refresh And upon reconnection with last-event-id, missed events are replayed with at-least-once delivery and clients deduplicate by event_id
Link Single-Use and Post-Use Invalidation
Given a BuddyLink is used to successfully claim a seat When the link is visited again or the claim endpoint is retried Then the link state is USED and further claims are rejected with 410 USED while returning the original reservation reference And the link cannot be reactivated, and its TTL does not exceed its configured expiry
Lifecycle Event Persistence and Auditability
Given any hold lifecycle transition occurs (created, viewed, claimed, expired, cancelled, auto-released, conflict_failed) When the event is stored Then the system persists an append-only record with fields: event_id, hold_id, class_id, link_token_hash, actor, previous_state, new_state, occurred_at (UTC), origin (API/UI), request_id, version And records are queryable by hold_id and class_id with stable ordering by occurred_at then event_id And sensitive tokens are not stored in plaintext and data retention follows policy for reporting and support
Invite, Reminder, and Status Notifications
"As a client and studio owner, I want timely invites and status updates so that friends convert before expiry and everyone knows the booking outcome."
Description

Send branded invite messages containing the BuddyLink via email, SMS, and push (where available), with delivery tracking and link click analytics. Provide optional “expiring soon” reminders and post-claim confirmations to both host and invitee. Allow studios to customize templates, sender profiles, and throttle rules; honor user communication preferences and regulatory requirements (e.g., SMS opt-in, unsubscribe). Ensure time zone–aware scheduling and deduplicate messages when the link is claimed or canceled to reduce noise. Expose notification events to the timeline in ClassTap and support resends from the host and admin views.

Acceptance Criteria
Multi-Channel BuddyLink Invite Delivery with Tracking
Given a host sends a BuddyLink invite with channels selected [email, SMS, push] and the recipient has valid contact points When the system processes the invite Then it sends the invite in each available selected channel within 30 seconds And each message contains a unique, trackable BuddyLink tied to {inviteId, channel} And delivery status per channel (queued, sent, delivered, failed/bounced) is recorded within 2 minutes of provider callback And unavailable channels are skipped and logged with reason (e.g., no device token, invalid email, no SMS opt-in) And a unique click is recorded per recipient per invite when the BuddyLink is opened, with channel attribution
Time Zone–Aware Scheduling for Reminders and Sends
Given an invite has a hold expiration timestamp T and the studio has configured an "expiring soon" lead time And the invitee has a time zone set (else the studio time zone applies) When scheduling the expiring-soon reminder Then the reminder is scheduled for (T − lead time) in the resolved time zone And daylight saving transitions are respected; the scheduled civil time matches the resolved zone And the actual send time deviates by no more than 60 seconds from the scheduled time And all stored and displayed timestamps include the time zone offset
Reminder Suppression on Claim or Cancel
Given an expiring-soon reminder job is pending for an invite When the invite is claimed or canceled before the reminder send time Then the reminder is not sent And the job is marked canceled with cause [claimed|canceled] in logs and timeline And no further reminders for that invite are scheduled
Post-Claim Confirmations to Host and Invitee
Given an invitee successfully claims a BuddyLink and the server confirms the claim When the claim event is processed Then a confirmation is sent to the invitee and the host via each of their preferred channels within 30 seconds, subject to consent And the confirmation includes class name, date/time, location or stream URL, and seat status And exactly one confirmation set is sent per claim (idempotent across retries) And the timeline shows both confirmation sends with channel and delivery status
Templates, Branding, and Sender Profiles
Given a studio has active templates, branding assets, and sender profiles configured for email and SMS When an invite, reminder, or confirmation is sent Then the message renders using the studio’s active template and branding and uses the configured sender profile for that channel And the template version and sender profile ID used are recorded with the message And a preview render for the same inputs matches the sent content excluding provider-added headers/footers
Preferences, Consent, and Compliance Enforcement
Given recipient communication preferences and consent states are stored per channel When attempting to send a notification Then SMS is sent only if SMS opt-in is present and not revoked; otherwise SMS is suppressed with reason recorded And email includes an unsubscribe link and honors unsubscribed state; email is suppressed if unsubscribed And SMS includes opt-out keyword instructions where required by region And messages are not sent using sender IDs or to regions prohibited by current compliance rules; attempts are logged and suppressed
Deduplication, Throttling, and Resend Logging
Given throttle rules are configured (e.g., max N messages per recipient per event per 24h) When the system or a host/admin attempts to send or resend a notification Then deduplication prevents sending more than one message per channel for the same event within a 5-minute window And throttle limits are enforced; excess messages are not sent and the UI receives a descriptive error And if a resend is initiated and the BuddyLink is still valid, the existing link is reused; if expired, a new link is generated and tracked And all sends, suppressions, and resends are recorded on the ClassTap timeline with actor, timestamp, channel, and outcome
Admin Controls and Business Rules
"As a studio admin, I want to configure BuddyLink rules and limits so that the feature aligns with our policies, improves conversions, and prevents abuse."
Description

Provide a centralized admin panel to enable/disable BuddyLink globally or per location/class, configure default hold durations, max buddy seats, per-user/day limits, allowed payment options (e.g., host-pay), and adjacency preferences. Allow eligibility constraints (e.g., members only, packages required) and channel controls (which notification types are allowed). Include preview of invite templates and live countdown behavior. Surface key metrics (invites sent, claim rate, time-to-claim, revenue attributed) and export/reporting hooks. Enforce role-based access and audit logging for configuration changes. Support feature flags, environment toggles, and safe rollout (beta cohorts).

Acceptance Criteria
Global/Per-Entity Toggle and Feature Flag Rollout Precedence
Given the environment toggle for BuddyLink is OFF When any user accesses any location or class Then BuddyLink UI components and APIs are not available in that environment Given the environment toggle is ON and a feature flag cohort rollout is set to 20% When 10,000 eligible sessions occur Then 18% to 22% of sessions expose BuddyLink and the remainder do not Given the environment toggle is ON and a user is not in any enabled feature flag cohort When the user visits eligible classes Then BuddyLink is not exposed Given the environment toggle is ON, feature flag includes the user, global toggle is ON, and a location override disables BuddyLink When the user views a class at that location Then BuddyLink is not exposed for that location Given the environment toggle is ON, feature flag includes the user, global toggle is OFF, and a class override enables BuddyLink When the user views that class Then BuddyLink is exposed for that class only Given an admin executes the emergency "Disable BuddyLink" rollback When the action is confirmed Then exposure drops to 0% within 60 seconds across all nodes and caches Rule: Toggle precedence = Environment OFF overrides all; else Feature Flag gating applies; within admin scope the most specific override wins (Class > Location > Global)
Hold Duration Configuration and Live Countdown Preview
Given the default hold duration is set to 10 minutes When the admin opens the countdown preview Then the timer displays 10:00 and decrements at 1-second intervals with drift <= 1s per minute Given a class-level hold override is set to 5 minutes When a BuddyLink is generated for that class Then the link expiry is 5 minutes from creation and the countdown reflects 05:00 at start Given a BuddyLink reaches its expiry time without a claim When the expiry threshold is crossed Then all held seats are released within 5 seconds and return to general inventory Given a BuddyLink is expired When the link is opened Then the UI shows "Link expired" and offers a CTA to standard booking without holding seats Rule: Changes to hold duration apply to new links only; existing links retain their originally set expiry
Max Buddy Seats, Per-User/Day Limits, and Adjacency Preferences
Given max buddy seats is configured to 2 When a creator attempts to reserve 3 buddy seats Then the UI prevents selection and the API returns 400 with error code BL_MAX_SEATS Given the per-user/day invite limit is 3 (account timezone) When a user attempts to create a 4th BuddyLink in the same calendar day Then creation is blocked with error code BL_DAILY_INVITE_LIMIT and no hold is created Given the per-user/day claim limit is 2 (account timezone) When a recipient attempts to claim a 3rd buddy seat in the same calendar day Then the claim is blocked with error code BL_DAILY_CLAIM_LIMIT and the seat remains available to others Given adjacency preference is set to Strict Adjacent When BuddyLink seats are allocated Then only contiguous seats are held; if insufficient contiguous seats exist, creation is blocked with error BL_ADJACENCY_UNAVAILABLE Given adjacency preference is set to Nearby within 2 seats When only non-contiguous seats within a gap of 2 are available Then those seats are held and the UI indicates a "nearby" allocation Rule: Changes to adjacency preferences and limits apply to new holds only; existing holds are unaffected
Eligibility Constraints and Allowed Payment Options Enforcement
Given eligibility is set to Members Only When a non-member attempts to create or claim a BuddyLink Then the action is blocked with 403 BL_INELIGIBLE and the UI displays an upgrade/purchase CTA Given eligibility is set to Package Required (e.g., Yoga-10) When the creator or claimant lacks the required package Then the flow requires purchasing the package or denies the action according to admin configuration Given allowed payment options = Host Pay only When initiating an invite Then only Host Pay is presented; upon buddy claim the host is charged and the buddy is not prompted for payment Given allowed payment options = Buddy Pays When a buddy claims a seat Then successful payment by the buddy is required before confirmation Given a disallowed payment option is attempted via API When the request is processed Then the API responds 403 BL_PAYMENT_OPTION_DISABLED and no seat is held or confirmed
Notification Channel Controls and Invite Template Preview
Given allowed channels are Email and SMS When sending or sharing a BuddyLink Then only Email and SMS options are available; Push is hidden and no push notifications are sent Given a message template contains tokens {class_name}, {start_time}, {location_name}, {sender_name} When the admin views the template preview Then tokens render with sample data and no raw tokens are displayed Given the admin clicks "Send test" for SMS from the preview When executed Then one test message is sent to the configured test number and is labeled TEST in logs; production sends are unaffected Given channels are toggled OFF for a specific location When a BuddyLink is created for that location Then the system sends no outbound notifications for the disabled channels Given deep linking is enabled When previewing the mobile template Then the invite link uses the configured app scheme and opens the class detail in the app on supported devices
Metrics, Attribution, and Export/Reporting Hooks
Given filters date range = last 7 days and location = Downtown When viewing the BuddyLink metrics dashboard Then it displays Invites Sent, Unique Claims, Claim Rate, Median Time-to-Claim, p90 Time-to-Claim, and Revenue Attributed Given the same filters When comparing dashboard counts to database ground-truth Then variance is <= 1% for counts and <= $0.01 per $100 for revenue totals Given attribution window is configured to 7 days When a claim occurs and payment is captured within 7 days Then the revenue is attributed to BuddyLink; payments captured after 7 days are excluded Given a CSV export is requested for the same filters When generated Then the file includes columns: invite_id, created_at, channel, inviter_user_id, claimant_user_id, class_id, location_id, claimed_at, time_to_claim_seconds, payment_option, revenue_amount, currency; and row counts match the dashboard totals Given a webhook for event "BuddyLinkClaimed" is configured When a claim occurs Then a POST is delivered within 10 seconds with the defined payload schema and up to 3 retries with exponential backoff on non-2xx responses
Role-Based Access Control and Audit Logging for Config Changes
Given roles Owner and Manager have the permission Manage BuddyLink Settings and Instructor does not When an Instructor accesses the BuddyLink admin page Then access is denied with 403 and no settings are exposed Given a Manager updates max buddy seats from 2 to 3 When the change is saved Then an audit log entry records actor_id, role, timestamp (UTC), IP, scope (global/location/class), field name, old value, new value, and environment Given audit logs are filtered by actor_id and date range When the filter is applied Then only matching immutable entries are displayed with no edit or delete capability Given an audit log export is requested When generated Then the CSV contents match the on-screen results and include a SHA-256 checksum of the file Given more than 5 unauthorized access attempts occur within 10 minutes by the same user When detected Then the events are logged and surfaced in security alerts
Waitlist-Integrated BuddyLink
"As a waitlisted client, I want BuddyLink to work when spots open so that my friend and I can join together without losing our place in line."
Description

Extend BuddyLink to waitlisted scenarios: allow a host to predefine a buddy intent while on the waitlist. When two adjacent seats become available, automatically generate and send a BuddyLink to the invitee and notify the host, starting a shorter hold timer. Respect waitlist order and promotion rules without leapfrogging other clients. If only one seat opens, offer the host a solo promotion while preserving buddy preference for a future opening (configurable). Ensure clean rollback if the buddy doesn’t claim in time and return both users to appropriate waitlist positions based on studio policy. Update rosters, invoices, and notifications accordingly.

Acceptance Criteria
Buddy Intent Added at Waitlist Entry
Given a client joins the waitlist for a seat-mapped class When the client selects "Bring a buddy" and submits invitee contact (email or SMS) and preferred channel Then the system validates contact format and stores buddy intent with timestamp, channel, and adjacency requirement on the waitlist entry And the client’s waitlist position remains unchanged and visible And the client can edit or cancel buddy intent until promotion; changes are audited with user, time, and field deltas And studio-configurable fields (custom message, buddy wait window) are captured if enabled and persisted
Auto-Dispatch BuddyLink on Two Adjacent Seats
Given two adjacent seats become available for a class instance and a waitlisted host with buddy intent is next eligible per studio policy When availability is detected Then the system generates a unique, single-use BuddyLink tied to the class instance and host And applies a hold timer equal to buddy_hold_minutes (default 10) that is less than single_hold_minutes And soft-holds exactly two adjacent seats, removing them from general inventory immediately And sends the BuddyLink to the invitee via the selected channel and a notification to the host within 60 seconds And displays the countdown timer to both host and invitee until claim or expiry
Waitlist Order and Promotion Integrity
Given a waitlist ordered by position with mixed entries (e.g., A#1 no-buddy, B#2 buddy, C#3 no-buddy) When seats become available Then promotions execute without leapfrogging earlier positions to satisfy buddy intent And if 2 seats open, A#1 is promoted first; only if ≥2 adjacent seats remain for B#2 does a BuddyLink dispatch to B’s invitee; otherwise C#3 is promoted next And if only 1 seat opens, the earliest position eligible under policy is promoted; buddy intents are preserved for future openings And each promotion decision is logged with a reason code and references to impacted waitlist entries
Single Seat Opening: Solo Promotion with Buddy Preference
Given a host with buddy intent and only one seat opens When offer_solo_on_single_open = true Then the host receives a solo promotion with a hold equal to single_hold_minutes And the buddy intent remains active for the same class instance within buddy_wait_window_minutes for a future adjacent opening When offer_solo_on_single_open = false Then the host is not promoted and retains position until two adjacent seats are available And all notifications reflect the configured behavior and are sent within 60 seconds
BuddyLink Expiry/Decline: Clean Rollback and Waitlist Reinstatement
Given a BuddyLink has been issued and two seats are on hold When the invitee does not claim before the hold expires or explicitly declines Then both seats are immediately released back to inventory And the host and invitee receive expiry/decline notifications within 60 seconds And reinstatement follows studio policy: if retain_host_position_on_expire, host returns to prior position; if requeue_to_end_on_expire, host is placed at the end; invitee has no new waitlist entry unless previously present, which is restored And any provisional invoices/authorizations are voided or released within payment_gateway_SLA_minutes and the audit log captures the rollback And no orphan holds, bookings, or charges remain
Seat Holds and Concurrency Safeguards
Given simultaneous cancellations, promotions, or BuddyLink events occur When adjacent seats are evaluated for a BuddyLink Then seat selection and holds are atomic, preventing double-booking across concurrent transactions And multiple opens of the same BuddyLink result in the first successful claim booking the seats; subsequent attempts show "Link already claimed or expired" And if a higher-priority waitlist promotion is committed in the same window, BuddyLink generation is aborted with no holds placed And system metrics record collisions, retries, and aborts for monitoring
Roster, Invoicing, and Notification Consistency
Given a successful BuddyLink claim within the hold window Then two bookings are created for host and invitee with adjacent seats, roster displays both with a Buddy tag, and waitlist entries are closed And invoices/charges follow pricing rules (credits/passes first, taxes/discounts applied) and receipts are sent to both within 2 minutes Given a solo promotion acceptance under a single-seat opening Then only the host booking and invoice are created, and the buddy preference remains tracked to trigger a future BuddyLink if within the configured window Given a rollback due to expiry/decline Then no active bookings exist, any provisional invoices are voided/refunded, the roster returns to its prior state, and notifications are sent

SwapCascade

When a premium seat opens, the system offers one-tap upgrades to the right clients in priority order (membership tier, booking time, preferences) and automatically backfills the vacated seats. Keeps front rows full, delights clients with upgrades, and removes staff juggling.

Requirements

Priority Ranking Engine
"As a studio owner, I want upgrade candidates prioritized by my business rules so that the right clients get first access to premium seats."
Description

Implements a deterministic, configurable ranking model that orders eligible clients for upgrade offers using membership tier, booking time, class and seating preferences, attendance history, and opt-in status. Supports tunable weights, tie-breakers, exclusion rules (e.g., already seated in premium zones), minimum eligibility windows, and studio-defined cohorts. Provides an API to fetch the next N candidates with real-time conflict checks. Integrates with ClassTap user profiles, memberships, bookings, and preference services, and enforces fairness via per-user and per-class throttle limits.

Acceptance Criteria
Deterministic Weighted Ranking and Tie-Breakers
Given a fixed set of eligible clients and a specific weights and tie-breaker configuration version, When the engine computes ranks multiple times without data changes, Then the returned order and scores are identical across runs. Given two or more clients with equal total weighted scores, When tie-breakers are applied, Then the configured sequence is enforced and the final order is stable across requests. Given the weights configuration is changed and published, When the engine recomputes ranks, Then the ordering reflects the new weights and differs from the prior order in a manner consistent with the change.
Exclusion Rules for Ineligible Clients
Given a client already seated in a premium zone for the class, When ranking, Then the client is excluded from eligibility. Given a client without upgrade opt-in, or with a canceled/invalid booking, or flagged by an exclusion rule, When ranking, Then the client is excluded. Given a client who previously declined an upgrade for the same class within the configured declineCooldown window, When ranking, Then the client is excluded. Given a client with a schedule conflict overlapping by at least the configured threshold, When ranking, Then the client is excluded.
Minimum Eligibility Window Enforcement
Given minTimeBeforeClassStart = 20 minutes, When now is within 20 minutes of class start, Then no candidates are returned for that class. Given minBookingAgeMinutes = 10, When a booking age is less than 10 minutes, Then that client is excluded until the window elapses. Given both windows are satisfied, When ranking, Then eligible clients are considered normally.
Studio-Defined Cohort Prioritization
Given cohort inclusion and weight rules configured for a class, When ranking, Then cohort-based filters and weights are applied to candidate eligibility and scoring. Given a client belongs to multiple cohorts, When ranking, Then the configured aggregation rule (e.g., max or sum) is used deterministically. Given no cohort is configured, When ranking, Then no cohort-based adjustments are applied.
API: Fetch Next N Candidates with Real-Time Conflict Checks
Given a request to GET /ranking/candidates with classId, seatType, and limit=N, When invoked, Then the API returns up to N ordered candidates with unique client identifiers and rank positions. Given a candidate becomes ineligible during evaluation (e.g., seat assignment changes, booking cancellation, throttle exceeded), When real-time conflict checks run, Then the candidate is skipped and replaced by the next eligible candidate in the response. Given identical parameters and a stable data snapshot, When the endpoint is called repeatedly, Then the responses are identical (idempotent). Given a class with up to 10,000 bookings and limit ≤ 10, When the endpoint is called under nominal load, Then p95 latency is ≤ 300 ms and p99 ≤ 700 ms.
Fairness Throttle Limits
Given perUserDailyOfferLimit = X, When a user has reached X upgrade offers in the past 24 hours, Then the user is excluded until the window resets. Given perUserPerClassOfferLimit = 1, When a user has already received an offer (accepted or declined) for the same class, Then the user is excluded from further offers for that class. Given perClassActiveOfferLimit = M, When the number of in-flight upgrade offers for the class is M, Then no additional candidates are returned until the count drops below M.
Data Integration Across Profile, Membership, Booking, and Preferences
Given a client has a valid booking and profile, When ranking, Then membership tier, booking time, seating and class preferences, attendance history, and opt-in status are retrieved from their respective services and used in scoring. Given a dependent service times out or returns an error for a client, When ranking, Then the engine applies the configured fallback (e.g., exclude client) without failing the entire request. Given cached data older than configured maxAge, When ranking, Then fresh data is fetched before computing scores.
One-Tap Upgrade Offers & Notifications
"As a booked client, I want a simple one-tap upgrade offer with clear details so that I can quickly claim a better seat without hassle."
Description

Delivers upgrade invitations via push, SMS, and email with secure, expiring deep links enabling one-tap accept or decline. Includes customizable templates, localized content, and response timers before auto-advancing to the next candidate. Presents seat preview, price delta, payment method to be charged, and policy notes. Respects user communication preferences and quiet hours, de-duplicates across channels/devices, tracks delivery/open/conversion events, and provides a responsive web fallback if the app is not installed.

Acceptance Criteria
Multi-Channel Offer Delivery with Secure Expiring Deep Links
Given an eligible candidate has at least one allowed communication channel When an upgrade offer is generated Then the system sends a single initial notification on the user’s highest-priority allowed channel (Push > SMS > Email) within 2 seconds of offer creation And the message includes a unique, signed, non-guessable deep link token bound to the user_id, class_id, and seat_id with expiration equal to the configured response timer And the deep link opens the native app offer screen if installed; otherwise it opens a responsive web offer screen And the token is single-use; any subsequent open returns an "Offer no longer available" view and HTTP 410 for web And the deep link contains no PII in clear text and is only served over HTTPS
One-Tap Accept and Decline with Payment and Policy Display
Given the user opens the offer screen via deep link Then the screen displays seat preview (row/label), class details (name/date/time), price delta with currency, default payment method (brand/last4), and upgrade policy notes before action buttons When the user taps Accept Then the system immediately attempts to capture the price delta on the displayed payment method and, on success, upgrades the seat and releases the original seat for backfill and shows a confirmation; an email receipt is sent And if price delta equals 0, no payment is attempted and the upgrade completes And if payment fails, the user sees a clear error and may retry with another saved method within the remaining timer; if the timer expires or the user abandons, no upgrade occurs and the cascade auto-advances When the user taps Decline Then their original seat is retained, no charge is made, the offer is closed, and the cascade auto-advances to the next candidate
Response Timer and Auto-Advance Priority Cascade
Given an offer is sent with a configured response timer When the timer elapses without acceptance Then the offer status becomes Expired, the deep link token is invalidated, and the next candidate in priority order (membership tier, booking time, preferences) is invited within 5 seconds When a candidate explicitly declines Then the next candidate is invited immediately When there are no remaining candidates Then the cascade ends and the premium seat returns to general availability per studio policy
Cross-Channel and Cross-Device Deduplication and Fallback
Given a notification is sent on the preferred channel When delivery confirmation is not received within 60 seconds and another allowed channel exists Then the system sends the offer on the next allowed channel When any channel’s message or deep link is opened Then all pending sends on other channels are canceled and marked suppressed to avoid duplicates And any subsequent attempt to open the offer from any other device/channel after Accept or Decline shows an "Offer already actioned" view And at most one active offer session exists per user per class at any time
Localization and Template Customization
Given channel templates (Push/SMS/Email) are configured per locale with placeholders When an offer is created for a user with locale L Then the outbound content renders using the L template and localizes currency, dates, and numbers per L And if a locale-specific template is missing, the default template is used without placeholder errors And template save is rejected if required placeholders are missing, with a validation message listing missing keys And the following placeholders are required and correctly rendered: {class_name}, {start_time_local}, {seat_label}, {price_delta}, {payment_method}, {expires_at_local}, {upgrade_link}, {policy_note}
Communication Preferences and Quiet Hours Handling
Given a user has specified allowed channels and quiet hours in their timezone When an upgrade offer is ready to send Then the system sends only via channels the user has allowed that are not in quiet hours And if all allowed channels are currently in quiet hours, the system does not notify the user, records skip reason "Quiet hours", and immediately advances to the next candidate And if a user has opted out or enabled DND on a channel, no message is sent on that channel and the skip is logged with reason "Opted out/DND"
Event Tracking and Reporting
Given an offer lifecycle progresses When key milestones occur Then the system emits idempotent events with ISO-8601 timestamps and identifiers, including: OFFER_CREATED, MESSAGE_SENT(channel), DELIVERY_CONFIRMED(channel), OPENED(channel), ACCEPTED, DECLINED, EXPIRED, PAYMENT_CAPTURED, PAYMENT_FAILED, AUTO_ADVANCED, SKIPPED(reason) And each event includes user_id, class_id, seat_id, offer_id, channel (where applicable), priority_rank, price_delta, locale, and response_time_ms (for ACCEPTED/DECLINED) And events are deduplicated by offer_id + event_type so retries do not create duplicates And events are queryable in the dashboard within 1 minute of occurrence and exportable via API
Atomic Cascade & Seat Backfill
"As an operations manager, I want the system to automatically upgrade and backfill seats reliably so that staff don’t have to manually juggle seating changes."
Description

Orchestrates the end-to-end cascade when a premium seat frees up: locks inventory, sends offers in priority order, upgrades the acceptor, releases and immediately backfills the vacated seat to the next eligible attendee, and repeats until no seats remain or candidates are exhausted. Guarantees atomicity and idempotency to prevent double-bookings using short-lived seat locks and transaction fences. Handles concurrency, retries, race conditions, cutoff times before class, capacity changes, and instructor overrides. Emits structured events for observability at each cascade step.

Acceptance Criteria
Priority-Ordered Upgrade Offer Dispatch
Given a premium seat is freed and a candidate pool exists And candidates are ranked by membership tier (desc), booking time (asc), and preference match (desc) with a deterministic tiebreaker When the cascade is triggered Then exactly one upgrade offer is sent to the highest-priority eligible candidate And no lower-priority offers are sent until the current offer is accepted or expires And ineligible candidates are skipped with a recorded reason code And only one active offer per freed seat exists at any time
Atomic Upgrade and Immediate Backfill
Given a non-premium attendee accepts an upgrade offer to a premium seat When the upgrade is committed Then the attendee’s original seat is released and assigned to the highest-priority eligible attendee in the same atomic transaction And the roster reflects no gaps or duplicate seat holders at any point of commit And the cascade repeats offer → accept/expire → upgrade → backfill until no premium seats remain or no eligible candidates are left
Idempotent and Concurrency-Safe Cascade Execution
Given multiple concurrent triggers or retries exist for the same freed premium seat When the cascade processes these triggers Then a single cascade instance acquires the transaction fence and performs state changes And duplicate processing attempts complete without altering the final state And no double-bookings occur under a load test of ≥100 concurrent triggers and ≥10 freed seats And repeated invocation with the same cascade ID produces the same final assignments
Seat Locking with TTL and Recovery
Given a seat is being offered or upgraded When a lock is acquired Then the lock is exclusive across services and nodes And the lock TTL is configurable with a default ≤15 seconds And if the process crashes or the TTL expires, the seat is returned to available or the previous holder within 5 seconds And lock acquisition, refresh, and release are recorded with correlation IDs
Offer Acceptance Window and Class Cutoff
Given an offer acceptance window (default 10 minutes) and a class cutoff (default 30 minutes before start) When an offer is sent Then the offer expires at the earlier of acceptance window or cutoff time And upon expiry, the next eligible candidate is offered within 3 seconds And no new offers are initiated after the cutoff time And expired offers cannot be redeemed via any client or API
Capacity Changes and Instructor Overrides During Cascade
Given an active cascade is running When the instructor changes premium capacity or reorders/skips candidates Then the cascade recalculates eligibility and continues from the new state without restarting And manual assignments bypass the queue while emitting override audit events And if capacity decreases below current holds, the lowest-priority holds are released with notification to affected users
Structured Events and Metrics for Observability
Given observability requirements for SwapCascade When a cascade runs Then structured events are emitted for seat_freed, offer_sent, offer_accepted, offer_declined, offer_expired, upgrade_committed, seat_backfilled, cascade_completed, and cascade_aborted And each event includes correlation_id, cascade_id, seat_id, attendee_id (when applicable), priority_rank, and timestamps And metrics capture offers_sent, accept_rate, backfill_latency_p50/p95, active_locks, retries, and failures And events are delivered at-least-once with idempotency keys for downstream consumers
Upgrade Payment & Billing Adjustments
"As a client, I want the price difference handled automatically using my saved payment method so that upgrading is effortless and transparent."
Description

Calculates and processes any price differences resulting from upgrades, supporting membership entitlements, comp rules, credits, taxes, and promotional limits. Performs pre-authorization or instant charges against stored payment tokens with SCA where required, and issues refunds or credits when moving from paid to included seats. Creates itemized ledger entries, updates invoices/receipts, syncs to accounting exports, and handles failures by skipping to the next candidate with appropriate notifications. Complies with PCI-DSS; no raw card data stored.

Acceptance Criteria
Instant Charge for Upgrade Price Difference (Taxes & Promotions)
Given a client with a valid stored payment token accepts a one-tap upgrade to a higher-priced seat And the organization has a configured tax rule and promotion limits When the system calculates the upgrade price delta Then eligible discounts are applied once per booking and promotional limits are enforced And tax is calculated on the net delta per configured inclusive/exclusive rules and rounding And the net delta is charged instantly to the stored token And on successful capture the booking is upgraded, a receipt is sent within 60 seconds, and the PSP transaction ID is linked to the ledger entry
Membership-Included Upgrade with Refund/Credit
Given a client with an active membership entitlement that includes the premium seat accepts an upgrade And the original seat had a payment recorded When the entitlement validation passes Then no new charge is attempted And the original payment amount (including applicable tax) is refunded or converted to platform credit per organization setting within 10 minutes And the invoice is updated to show a credit line for the original seat and $0 upgrade cost And the membership allowance counter is decremented and auditable
Apply Credits and Comp Rules Before Charging Card
Given a client has available class credits and/or comp upgrade allowances And an upgrade has a positive price delta When the upgrade is accepted Then the system applies comp rules and credits in the configured order until the net delta is zero or benefits are exhausted And only the remaining balance plus tax (if any) is charged to the stored token And if benefits fully cover the delta, no card charge occurs And usage of credits/comps is decremented and recorded in the ledger with references to the booking
SCA Challenge and Pre-Authorization Flow
Given the PSP indicates SCA is required or the org requires pre-authorization for upgrades When the client accepts an upgrade that requires payment Then a 3DS/SCA challenge is initiated and a pre-authorization hold for the estimated net delta is requested And if the challenge succeeds within 5 minutes, the booking is upgraded and the hold is captured or adjusted to the final amount And if the challenge fails or times out, no seat change occurs, any holds are released, and the client is notified with a retry option (if enabled)
Payment Failure Handling and Candidate Skip
Given an upgrade attempt requires a charge or pre-authorization When the payment is declined, times out, or errors at the PSP Then the candidate is marked payment_failed with PSP error code recorded And the booking remains unchanged and any holds are released And the next candidate in priority order is notified within 30 seconds And the original candidate receives a failure notification with guidance to update payment method And the event is logged for audit with correlation IDs
Ledger, Invoices/Receipts, and Accounting Export Sync
Given an upgrade results in any financial action (charge, refund, credit, credit usage) When the transaction completes Then itemized ledger entries are created for upgrade delta, tax, refunds, and credit usage with unique IDs and PSP references And the related invoice/receipt is updated or issued and emailed to the client automatically And the accounting export includes these entries on the next scheduled sync and passes schema validation And totals reconcile to within $0.01 of PSP settlement amounts
PCI-DSS Compliance and No Raw Card Data
Given payments are processed using stored tokens from the PSP When storing data, logging events, or generating exports Then no raw PAN, CVV, or unmasked expiration data is stored, logged, or exported And only token IDs and masked card details (brand and last4) are visible to authorized roles And database schemas, logs, and backups contain no cardholder data fields And automated quarterly scans report zero findings for cardholder data leakage
Admin Rule Configuration & Controls
"As a studio admin, I want to configure how upgrades work for my business so that the feature aligns with my policies and client expectations."
Description

Adds a studio-facing settings panel to enable SwapCascade, define eligible classes/seat zones, set priority weights and tie-breakers, choose notification channels and response windows, cap upgrades per class/day, set blackout dates, and enable opt-in defaults (e.g., auto-upgrade up to a chosen price delta). Includes seat map tools for marking premium zones and front rows. Provides preview/simulation to test cascades and see candidate order before enabling. All changes are audited, versioned, and can have effective dates.

Acceptance Criteria
Global and Class-Level Enablement & Eligibility
Given I am an admin with permissions When I toggle SwapCascade to Enabled and save Then the system persists the global Enabled state and displays a success confirmation And the configuration API reflects Enabled=true within 2 seconds Given SwapCascade is globally Enabled When I select specific class types and individual classes as eligible and save Then only those classes display the SwapCascade indicator in the scheduler and detail views And non-eligible classes do not trigger SwapCascade behavior when premium seats open Given SwapCascade is globally Disabled When any class-level eligibility exists Then no SwapCascade indicators are shown and no upgrade offers are generated
Seat Map Premium Zone & Front Row Marking
Given a class template with a seat map When I select seats to mark as Premium and optionally flag a Front Row subset and save Then the system persists the zone metadata to the class template version with timestamp and editor identity And the seat map visually renders Premium and Front Row with distinct legends in preview And overlapping or duplicate zone assignments are rejected with an inline validation message Given a scheduled class instance inherits the template When I open its seat map Then the Premium and Front Row designations are visible and read-only unless explicitly overridden by an admin override action And only seats marked Premium are considered as upgrade targets by SwapCascade
Priority Weights and Tie-Breakers Configuration
Given I am on the SwapCascade Priority Settings When I set numeric weights for membership tier, booking time, and stated seating preferences within the allowed range shown by the UI and save Then the system validates required fields and ranges, prevents save on invalid input, and persists the weights with a version id Given weights are saved When I define a deterministic tie-breaker order (e.g., tenure, last-upgrade date, random) and save Then the system stores the exact order and displays it in the summary And the candidate ranking engine uses the saved weights and tie-breakers for all subsequent computations
Candidate Order Preview & Simulation
Given I have configured eligibility, zones, weights, and tie-breakers When I click Preview with a selected future class and sample roster Then the system displays an ordered candidate list with position numbers and a per-candidate rationale (top 3 scoring factors) within 1 second Given the Preview is visible When I adjust a weight or tie-breaker and re-run Then the candidate order updates accordingly and highlights the changes in rationale Given a premium seat opening is simulated for the selected class When I run a Cascade Simulation Then the simulation shows the upgrade chain and backfill sequence, honoring caps, blackouts, opt-in status, and price-delta limits And no simulated offer violates configured limits
Client Communication & Response Window Settings
Given I am on Notification Settings When I select one or more notification channels (email, SMS, in-app) for upgrade offers and save Then the system requires at least one channel before enabling SwapCascade and persists the selected channels Given I set a response window duration (e.g., 5–60 minutes) and save When an upgrade offer is sent Then the offer expires at the configured duration and is withdrawn if not accepted before expiry Given Auto-Upgrade Defaults are enabled When I set the default opt-in for new clients and a maximum price delta (fixed or percentage) and save Then clients with auto-upgrade ON are upgraded without prompts only if the total price increase is within the configured delta And if the expected delta exceeds the limit, a standard offer is sent instead of auto-upgrade
Operational Limits: Upgrade Caps and Blackout Dates
Given I configure an Upgrade Cap per class instance and a Daily Cap per studio and save When SwapCascade runs for any class Then the engine does not execute upgrades beyond either cap and logs attempts blocked by caps And the UI shows remaining upgrade slots for the class/day in the admin dashboard Given I define blackout dates/times (single dates, ranges, or recurring rules) and save When a premium seat opens during a blackout window Then no SwapCascade offers are sent and the event is logged as suppressed by blackout And the scheduler surfaces a "Blackout Active" indicator for affected classes
Audit Trail, Versioning, and Effective Dates
Given I have modified any SwapCascade setting When I save the changes Then an audit entry is created capturing actor, timestamp, affected fields (before/after), and change reason (if provided) And the change is assigned a version number Given version history exists When I view the history Then I can filter by date, actor, and setting area, and compare two versions field-by-field Given I schedule a future Effective Date/Time for a configuration version and save When the effective time is reached Then the new version becomes active automatically and is used by the engine for ranking and notifications And the preview tool can be switched to simulate either current or future-dated versions
Analytics & Audit Trail
"As a studio owner, I want detailed metrics and logs about upgrades so that I can measure performance and optimize my rules."
Description

Provides dashboards and exportable reports showing upgrade offer volume, acceptance rate, time-to-claim, revenue lift from upcharges, seat occupancy by zone, reduction in staff interventions, and impact on no-shows. Breakdowns by class, instructor, time, and membership tier. Captures a tamper-evident audit log of offers created, delivered, accepted, expired, skipped, and payment outcomes for compliance and support. Streams structured events to analytics for cohort analysis and A/B testing.

Acceptance Criteria
Dashboard Metrics Accuracy and Breakdowns
Given an account timezone, date range, and filters for class, instructor, time block, and membership tier When the Analytics dashboard is loaded Then it displays offer volume, acceptance rate, avg/median/P90 time-to-claim, revenue lift from upcharges, seat occupancy by zone, staff interventions, and no-show impact with values matching source-of-truth queries within ±0.5% And each metric respects the selected filters and date range with totals equaling the sum of visible breakdowns within 0.1% rounding error And metric cards load within 2s (p95) for <=50k events and within 8s (p95) for <=1M events And all timestamps reflect the account timezone and tooltips show the UTC equivalent And clicking any metric opens a drilldown list of contributing classes whose counts reconcile to the aggregate within 0.1%
Report Export: CSV Completeness and Fidelity
Given applied dashboard filters and a selected reporting period When the user exports analytics as CSV Then the file contains one row per class session with columns: offer_volume, acceptance_rate, avg_time_to_claim_sec, p90_time_to_claim_sec, revenue_lift_cents, occupancy_front_row_pct, occupancy_all_zones_pct, staff_interventions_count, no_show_rate, class_id, instructor_id, time_block, membership_tier And the first lines of the file include metadata comments (prefixed with #) capturing export_time_utc, account_timezone, date_range, applied_filters, schema_version And numeric formats use '.' as decimal, strings are UTF-8, timestamps are ISO 8601 UTC And exports up to 200k rows complete within 60s; larger exports run asynchronously and deliver a secure link via email within 15 minutes And users without Analytics Export permission cannot export and receive a 403 error with no file generated And exported aggregates reconcile to dashboard values within ±0.5% for the same filters
Tamper-Evident Audit Log for Offer Lifecycle
Given SwapCascade offer lifecycle events occur When auditing the log Then every event type (created, delivered, accepted, expired, skipped, payment_succeeded, payment_failed, refunded, chargeback) is recorded with fields: event_id, offer_id, class_id, seat_id, customer_id_hash, actor, action, reason_code, amount_cents, currency, timestamp_utc, prev_hash, hash, signature And the log is append-only; delete or update attempts are blocked and produce an integrity alert And a daily chain verification detects any break in the hash chain and surfaces an incident within 15 minutes And access is read-only to Admin and Compliance roles; Support role can query via audited just-in-time access And PII (email, phone, card_last4) is masked in the log; authorized re-identification requires elevated approval and is logged And records are retained for 7 years and exportable as signed NDJSON with a verifiable Merkle root
Analytics Event Streaming and Schema Governance
Given an offer lifecycle change occurs When emitting analytics events Then a structured event is published to the event bus within 5s (p95) with fields: event_id, occurred_at, emitted_at, offer_id, class_id, seat_id, customer_id_hash, action, amount_cents, currency, experiment_id, variant, schema_version, idempotency_key And delivery is at-least-once with zero loss verified in controlled failover tests; duplicates are deduplicated downstream using (event_id, idempotency_key) And events conform to a registered schema; incompatible changes require a new schema_version and pass CI validation And the pipeline buffers for 24h during outages and auto-retries until delivery resumes; backlog drains within 2h after recovery for <=5M events And PII fields are excluded or hashed per data policy and marked with privacy tags
Revenue Lift Attribution from Upcharges
Given a client upgrades to a premium seat with an upcharge When calculating revenue lift Then revenue_lift_cents equals (post_upgrade_price_cents - pre_upgrade_baseline_price_cents) excluding taxes and fees, net of discounts and credits And refunds, partial refunds, and chargebacks adjust revenue_lift_cents within 24h of the event and appear as negatives in reports And multi-currency transactions convert to the account currency using the session-date FX rate; totals reconcile to the payments ledger within ±0.1% And membership-included upgrades with zero upcharge report zero lift; comped upgrades are excluded from lift And dashboard and export show identical lift totals for identical filters
Seat Occupancy by Zone and Backfill Impact
Given a zoned class session with SwapCascade enabled When upgrades and backfills occur Then the system records occupancy_per_zone before cascade, after upgrades, and after backfill and computes front_row_fill_pct, delta_occupancy_per_zone, and backfill_success_rate And seats marked as blocked or instructor-held are excluded from occupancy calculations And non-zoned sessions are labeled N/A and excluded from zone-based aggregates And per-class occupancy totals reconcile to attendee counts within ±1 seat And the dashboard shows a time series of zone occupancy for the session day with tooltips linking to the audit log entries that created the changes
Staff Intervention Reduction and No-Show Impact
Given a baseline period and current period with optional A/B cohorts When computing impact metrics Then staff_interventions_count includes manual seat moves, manual waitlist promotions, and manual refunds related to upgrades where actor != system and action in {move_seat, promote_waitlist, refund} And reduction_per_100_bookings equals (baseline_per_100 - current_per_100) with 95% confidence intervals; A/B results include uplift and p-value <= 0.05 when significant And no_show_rate equals (booked - attended - cancelled_before_cutoff) / booked, counting late cancels as no-shows; reported by membership tier and time block And methodology definitions are available via tooltip and included in export metadata And metrics recalculate within 15 minutes of relevant source changes and reconcile to attendance logs within ±0.5%

Seat Heatmap

Visual analytics reveal which seats and rows fill fastest, churn most, or carry higher no-show risk by time and class type. Get layout and pricing recommendations—like premium front-row pricing or incentives for cold spots—to lift revenue and reduce empty equipment.

Requirements

Seating Layout Mapping
"As a studio owner, I want to configure precise seat layouts for each room and class type so that the heatmap reflects real seat positions and equipment usage."
Description

Provide tools to define, edit, and version seat layouts per room and class type, including seat IDs, rows, labels, and equipment mapping (e.g., bike numbers, reformers, mats). Support layout templates, seat capacity constraints, and validation for unique identifiers. Enable applying different layouts to schedules and specific class instances, and ensure layout data syncs with booking capacity to prevent double-bookings and maintain accurate analytics foundations.

Acceptance Criteria
Create Layout with Unique Seats and Equipment Map
Given I am on the Create Layout screen for a specific room and class type When I add rows and define seats with seatId, row label, display label, and equipmentId Then the system validates seatId uniqueness within the layout before allowing save And seatId must match the allowed pattern [A-Za-z0-9-_.]{1,20} And the layout capacity equals the count of active seats at save time And Save returns a 201 response with layoutId and version "v1" And the new layout appears in the room’s layout list with correct seat count and equipment mapping preview
Versioned Layout Update Preserves Historical Bookings
Given a published layout version is assigned to past and future classes When I edit the layout and choose Save as New Version Then the system creates version v(N+1) in Draft state and retains vN for existing classes And past classes remain linked to vN with no seat reassignment And publishing v(N+1) does not modify past classes And the changelog records added/removed/renamed seats with timestamp, editor, and reason
Assign Layout to Schedule and Override for Class Instance
Given a class type has a default layout and a room has multiple layouts When I assign a layout to a recurring schedule Then all newly created future instances inherit that layout and capacity And when I override a single future instance with another layout Then the instance reflects the override and updates capacity immediately And if existing bookings exceed the override layout capacity, the override is blocked with an error stating how many bookings exceed capacity
Capacity Sync Prevents Overbook and Adjusts Waitlist
Given a future class has an assigned layout and waitlist enabled When I activate additional seats and publish the layout version before bookings close Then class capacity updates to the new seat count and up to the new available slots auto-promote the earliest waitlisted attendees per policy with notifications sent When I attempt to reduce seats so capacity would drop below current confirmed bookings Then the change is blocked with an error detailing the number of seats to free before publishing And under concurrent booking attempts for the same seatId, only one booking succeeds and the other receives a clear seat-unavailable message
Template Creation and Application Across Rooms/Class Types
Given I create a layout and choose Save as Template with a unique template name When I view compatible rooms for application Then only rooms with matching required equipment types/counts are enabled for selection and incompatible rooms show a mismatch tooltip And applying the template clones it into a room-specific layout with seatId pattern preserved and new layoutId assigned And role permissions enforce that only Admins can create/update templates while Instructors can apply them
Unique Identifier Validation and Bulk Import
Given I upload a CSV or JSON defining seats for a layout When the file contains duplicate seatId values, invalid characters, blank labels, or unknown equipmentId Then validation fails with a list of errors including line numbers and fields and no data is saved And reserved identifiers such as "NULL", "NONE", or empty strings are rejected And when the file passes validation, all seats are created, row labels assigned, and seatId values are normalized (trim, uppercase) atomically
Analytics Continuity Across Layout Versions
Given seat positions or identifiers may change between layout versions When a seat is renamed or moved but mapped to the same physicalEquipmentId Then analytics attribution aggregates across versions for that physical seat And when a seat is split or merged, a new analytics lineage is created starting at the publish time of the change And the heatmap uses the correct layout version for each class (historical vs future) without data loss And the export API returns seatId, layoutVersion, and physicalEquipmentId per booking for traceability
Seat Event Data Pipeline
"As an operations manager, I want accurate seat-level event data so that the heatmap analytics are trustworthy and actionable."
Description

Implement a reliable, real-time data pipeline that captures seat-level events across the booking lifecycle, including selection, booking, cancellations, waitlist promotions, check-ins, and no-show flags. Normalize events to a unified schema keyed by seat ID, class instance, and user, and store history for time-series analysis. Include data quality rules, deduplication, backfill for migrated bookings, and privacy-safe aggregation to power metrics like fill rate, churn, and attendance without exposing PII.

Acceptance Criteria
Real-time seat event ingestion and normalization
Given a live class instance, when a user performs any seat action (seat_selected, seat_booked, seat_canceled, waitlist_promoted, check_in, no_show), then an event is emitted and ingested with p99 end-to-end latency <= 5 seconds. And the event conforms to the unified schema with required fields: event_id, event_type, seat_id, class_instance_id, user_id, event_time (UTC ISO-8601), source, version. And events missing any required field are rejected to a dead-letter queue (DLQ) and do not appear in analytics tables. And events are persisted append-only with retention >= 24 months and partitioned by event_date (UTC) and class_instance_id.
Idempotent deduplication and event ordering
Given duplicate events sharing the same idempotency_key OR composite key (source_event_id, event_type, seat_id, class_instance_id, user_id), when ingested within a 24-hour window, then only one analytics record exists. And for a given (seat_id, class_instance_id, user_id), events are ordered by event_time; late-arriving events are re-ordered within a 48-hour lateness window. And deterministic replays of the same source window produce byte-identical outputs in analytics tables.
Backfill and migration completeness
Given a historical export covering a date range, when backfill runs, then >= 99.5% of source records are loaded as normalized events, reconciled per day and per class_instance_id. And backfilled events are tagged backfill=true with backfill_batch_id; existing events are not duplicated (idempotent re-runs allowed). And backfill throughput sustains >= 50,000 events per minute without breaching real-time ingestion SLAs. And the job fails with a report if completeness < 99.5% or schema validation error rate > 0.5%.
Data quality validation, referential integrity, and alerting
Given incoming events, when validation executes, then required fields are present and typed, enums are valid, and event_time is not more than 10 minutes in the future. And referential integrity holds for class_instance_id to class catalog and seat_id to layout map; violations are routed to DLQ with reason codes. And rolling 24h validation failure rate < 0.1%; if failures exceed 0.5% in any 15-minute window, an alert is sent within 2 minutes. And DLQ supports replay; successful replays remove items and are auditable with correlation IDs.
Privacy-safe data handling and access controls
Given analytics event and aggregate tables, then no PII fields (email, phone, legal name, payment details) are stored; user_id is pseudonymous and stable across sessions. And data in transit uses TLS 1.2+ and at rest uses AES-256 encryption; access is restricted via RBAC to authorized roles only; all access is logged with retention >= 12 months. And aggregate outputs contain only seat_id, class_instance_id, time buckets, and counts/metrics; any export enforces k-anonymity with k>=3 distinct users to prevent re-identification.
Waitlist promotion-to-payment linkage and seat state transitions
Given a waitlisted user is promoted, when payment succeeds, then events exist in order: waitlist_promoted -> payment_succeeded -> seat_booked, all sharing the same (seat_id, class_instance_id, user_id) lineage. And if payment fails or times out, then a payment_failed event is recorded and the seat remains unbooked; no seat_booked event is produced. And seat state transitions are valid per finite-state model; invalid transitions (e.g., check_in before seat_booked) are rejected to DLQ with reason.
Metrics readiness for fill rate, churn, and attendance
Given a synthetic validation dataset with known outcomes, when aggregates are computed, then: fill_rate = booked_seats/total_seats, churn_per_seat = cancellations_per_seat, and no_show_rate = no_shows/expected_attendees each match expected values within ±0.5%. And aggregates exclude seats marked out_of_service and admin/test bookings. And aggregates update in near-real-time with p95 end-to-end refresh latency <= 60 seconds from event ingestion.
Interactive Heatmap & Filters
"As an instructor, I want an easy-to-read heatmap with filters so that I can quickly identify hot and cold seats and adjust setup and coaching."
Description

Deliver an interactive, color-coded heatmap UI that visualizes seat- and row-level metrics such as fill speed, churn rate, and no-show probability over selectable date ranges. Provide filters for class type, instructor, room, daypart, and weekday/weekend, plus toggles for per-seat vs per-row views. Include tooltips with metric definitions and values, legends, accessibility-compliant color contrasts, responsive design for desktop and mobile, and export options (image/PDF) while maintaining performant rendering for large layouts.

Acceptance Criteria
Heatmap Rendering and Metric Selection Performance
Given a layout with up to 500 seats and a selected date range, When the user opens the Seat Heatmap, Then the heatmap renders all seat/row tiles with color-coding within 2 seconds and shows a loading state until complete. Given the default metric is Fill Speed, When the user switches the metric to Churn Rate or No‑Show Probability, Then the color scale and legend update to the new metric and the heatmap re-renders within 700 ms after data is returned. Given API values for the selected metric, When the heatmap displays per-seat/row values, Then each displayed value matches the API to two decimal places.
Filtering by Class Type, Instructor, Room, Daypart, Weekday/Weekend, and Date Range
Given multi-select filters for Class Type, Instructor, and Daypart, and single/multi-select for Room as configured, and a Date Range picker with presets (Last 7/30/90 days, Custom), When the user applies any combination of selections, Then the heatmap updates to reflect the intersection of all filters within 1 second after data is returned. Given active filters, When the user clears filters or selects Reset, Then all filters return to defaults (no filters + Last 30 days) and the heatmap reflects the default state. Given a custom date range is selected, When the user applies it, Then aggregated metrics reflect only events within that range and the applied range is displayed in the UI.
Per-Seat vs Per-Row Toggle and Correct Aggregation
Given the heatmap is in Per-Seat view, When the user toggles to Per-Row, Then the visualization switches to row-level tiles/bands within 500 ms and all metrics are aggregated per row according to the documented method shown in the tooltip. Given Per-Row view, When the user returns to Per-Seat, Then the prior zoom level and filters persist and the seat-level values match the API for the current filters. Given both views, When the user exports or switches metrics, Then the currently selected view (seat or row) is preserved in the output and UI state.
Tooltips with Metric Definitions and Values
Given a seat or row is hovered (desktop) or tapped (mobile), When the pointer/focus enters the tile, Then a tooltip appears within 150 ms showing: seat/row ID, selected metric name, value (with units/precision), date range, and a one-line metric definition including aggregation method. Given keyboard navigation, When the user tabs to a seat/row, Then the tooltip opens on focus and is announced to screen readers via ARIA with the same content. Given small viewports, When the tooltip would overflow off-screen, Then it repositions to remain fully visible without covering the focused tile.
Legend and Accessibility Compliance
Given a selected metric, When the heatmap renders, Then a legend is displayed with color bins and numeric ranges that match the current data domain and scale. Given any color-coded state, When checked with accessibility tools, Then text-to-background contrast on tiles and legend meets WCAG 2.1 AA (≥ 4.5:1), and interactive controls have visible focus indicators. Given users who cannot rely on color alone, When they interact with the heatmap, Then exact values are available via tooltips and screen readers so that information is not conveyed by color alone.
Responsive Behavior on Desktop and Mobile
Given a desktop viewport (≥1024px), When the heatmap loads, Then filters are visible in a sidebar and the heatmap fits without horizontal scroll, maintaining 60 FPS interaction while panning/hovering. Given a mobile viewport (≤768px), When the heatmap loads, Then filters collapse into a drawer, tap targets are at least 44px, and the heatmap supports vertical scroll without horizontal overflow. Given device rotation or viewport resize, When the orientation changes, Then the layout reflows within 500 ms and retains current filters, metric, date range, and view toggle state.
Export to Image and PDF with Current State
Given any current heatmap state (filters, date range, metric, legend, per-seat/row view), When the user selects Export as PNG, Then a 2x DPR image is generated within 3 seconds that visually matches the on-screen heatmap and includes the legend, title, and applied date range (excluding open tooltips). Given the same state, When the user selects Export as PDF, Then a landscape PDF (A4 or Letter per locale) is generated within 3 seconds with the same fidelity and metadata. Given exports on mobile and desktop, When the export completes, Then the file size is ≤5 MB for layouts up to 500 seats and colors/values match on-screen rendering.
No-Show Risk Scoring
"As a studio owner, I want visibility into seats with higher no-show risk so that I can send targeted reminders or adjust overbooking to reduce empty spots."
Description

Create a predictive scoring service that estimates no-show risk at the seat and class-instance level using historical attendance, time of day, class type, and recent booking behavior. Provide a fallback heuristic when data is sparse, expose confidence levels, and explain key drivers to build trust. Store scores per instance for auditing, refresh them on schedule changes, and integrate with reminder and overbooking policies so operators can target interventions for high-risk seats.

Acceptance Criteria
Seat-Instance Risk Score Generation
Given a class instance with one or more reserved seats When the no-show risk scoring service runs Then each reserved seat receives a risk score in the range [0.00, 1.00] with 2-decimal precision And features used include historical attendance, time of day, class type, and recent booking behavior (when available), recorded in the scoring metadata And 99.9% of reserved seats receive a score without error And for a 100-seat class, scoring completes in ≤ 3 seconds end-to-end
Sparse Data Fallback Heuristic
Given a reserved seat where attendee history < 3 attendances OR seat history < 5 occurrences in the past 90 days When scoring executes Then the fallback heuristic is used and flagged method="heuristic" And the heuristic score is computed from: 60% class-type 90-day base no-show rate, 30% time-of-day adjustment, 10% booking recency signal And the returned confidence ≤ 0.40 And the output schema includes fields: score, confidence, method, and timestamp
Confidence Score Exposure
Given any scored seat When retrieving the score via API or data store Then a confidence value between 0.00 and 1.00 is present And if method="model" with ≥ 50 similar historical examples used, confidence ≥ 0.60 And if method="heuristic", confidence ≤ 0.40 And confidence values are deterministic for the same inputs and modelVersion
Explainability: Key Drivers
Given any scored seat When requesting explanations Then the response includes the top 3 drivers, each with fields {name, direction, contributionPercent} And contributionPercent values sum to 100% ± 1% And driver names and values contain no PII (e.g., full name, email) And drivers are ordered by absolute contribution descending
Persistence and Auditing of Scores
Given a scoring run for a class instance When storing outputs Then each record includes classInstanceId, seatId, score, confidence, method, modelVersion, featureSnapshotHash, and createdAt And records are immutable; updates create a new version linked by supersedesId And records are retained for ≥ 180 days And querying by classInstanceId returns scores for up to 100 seats in ≤ 200 ms And past scores are reproducible using modelVersion and featureSnapshotHash
Automatic Refresh on Schedule Changes
Given a class instance where start time, capacity, instructor, or a waitlist-to-seat conversion changes When the change is persisted Then affected seat scores are recomputed within 2 minutes And previous scores are marked status="superseded" and excluded from downstream policy evaluation And a score-updated event is emitted for subscribers
Policy Integration for Reminders and Overbooking
Given a seat with risk score ≥ 0.70 and class start time between 24 and 4 hours from now When policies are evaluated Then a targeted reminder job is scheduled once per seat per class instance (no duplicates) And if overbooking is enabled with cap X, seats with average class risk ≥ 0.50 allow overbooking offers up to capacity + X without exceeding the cap And the same physical seat is never assigned to more than one active attendee And each intervention logs the scoreId and modelVersion used
Dynamic Seat Pricing Recommendations
"As a business owner, I want actionable pricing and incentive suggestions per seat or row so that I can increase revenue and reduce unused equipment."
Description

Offer algorithmic recommendations for premium pricing on high-demand seats and incentives (discounts, credits, bundles) for low-demand seats, with guardrails for maximum deltas, member-specific rules, and brand constraints. Provide a preview of estimated revenue and utilization uplift, allow selective application to future classes or schedules, and integrate with the existing pricing engine, checkout, invoicing, and audit logs with easy rollback.

Acceptance Criteria
Premium Pricing Recommendation for High-Demand Seats
Given historical seat heatmap indicates seat(s) demand at or above the configured high-demand threshold for a class template and time window When the admin opens Dynamic Seat Pricing Recommendations for upcoming sessions Then the system proposes a premium price per qualifying seat including seat ID, baseline price, recommended price, absolute delta, and percent delta And the percent delta is between the configured minimum premium step and the configured maximum premium delta And the recommended price respects brand price floor/ceiling and formatting constraints (e.g., price endings) And no recommendation is shown for seats already at or above ceiling or within the configured no-change tolerance of the computed recommendation
Incentive Recommendations for Low-Demand Seats
Given historical seat heatmap indicates seat(s) demand at or below the configured low-demand threshold for a class template and time window When the admin views Dynamic Seat Pricing Recommendations Then the system proposes an incentive for each qualifying seat using only allowed incentive types (percent discount, fixed credit, bundle) And the incentive respects configured guardrails (max discount %, min price floor, eligible bundles) and brand constraints And the UI displays seat ID, baseline price, incentive type, resulting price, and percent change And no incentive is proposed if it would breach guardrails or reduce price below the brand floor
Preview of Estimated Revenue and Utilization Uplift
Given the admin selects a subset of proposed seat-level recommendations to preview When the preview is generated Then the preview displays baseline vs projected totals for revenue ($ and %) and utilization (% of seats filled) at seat-, class-, and selection-level And the preview includes the number of sessions affected and the assumptions time range used And all totals sum correctly from seat-level to selection-level within rounding to 2 decimals And the preview renders within 2 seconds for up to 500 seats and 100 sessions And removing or adding a recommendation updates the preview totals consistently
Selective Application to Future Classes and Schedules
Given the admin chooses Apply and specifies filters (date range, location(s), class template(s), day/time windows) and whether to include already published future sessions When the admin confirms application Then pricing rules are created only for sessions starting after now that match the filters And existing bookings remain at their original prices; only future availability reflects new prices And the confirmation summary shows the count of sessions and seats affected, with an ID list downloadable And waitlist-to-payment offers use the active price at the time of offer generation And no rules are created outside the selected scope
End-to-End Integration with Pricing Engine, Checkout, and Invoicing
Given recommendations have been applied When a buyer selects an affected seat in checkout Then the displayed seat price equals the active rule-derived price and taxes/fees are computed on that price And invoice line items reflect the seat-level adjustment or incentive with correct labels/codes And coupons, credits, and bundles stack only per brand rules without exceeding guardrails And unaffected seats in the same session retain their original prices And pricing cache invalidates so that new prices are visible in checkout within 60 seconds of application
Member-Specific Rules and Brand Constraints Enforcement
Given member and non-member pricing rules and brand constraints are configured When recommendations are generated and applied Then proposed and applied prices for members respect member-specific caps/exemptions and for non-members respect general guardrails And checkout shows the correct price per user type and locks the price at reservation time And if overlapping rules exist, precedence is applied in order: member-specific > schedule-specific > template-level > default, resulting in a single final price per seat And no recommendation or applied price violates brand constraints (floors, ceilings, required endings, restricted incentives)
Audit Logging and One-Click Rollback
Given recommendations are applied to one or more sessions When the application completes Then an audit record is created capturing who, when, scope filters, rule IDs, before/after price deltas, and version And the admin can perform a one-click rollback that restores prior prices/rules for all affected future sessions and removes or supersedes applied rules And rollback does not alter past or paid invoices but updates unpaid future invoices/checkout to reflect restored prices within 60 seconds And a corresponding rollback audit entry is created with linkage to the original change And repeating the rollback action is idempotent (no further changes after first success)
Impact Tracking & A/B Testing
"As a studio owner, I want to measure the impact of seat-based changes so that I can double down on strategies that reliably improve revenue and attendance."
Description

Enable controlled experiments and post-implementation tracking to attribute changes in revenue, utilization, and no-show rates to applied seat pricing or layout recommendations. Provide dashboards with control vs treatment comparisons, cohort filters, and statistical significance indicators, plus scheduled reports and CSV exports. Tie results back to specific recommendations and classes to inform future optimization.

Acceptance Criteria
A/B Experiment Setup for Seat Pricing/Layout Recommendations
Given I am an authorized studio admin on ClassTap with access to Seat Heatmap experiments And I have selected a pricing or layout recommendation to test When I create a new experiment and define a control and at least one treatment variant And I choose the randomization unit (class session, row, or seat) and ramp percentage And I select primary metrics (revenue per class, seat utilization %, no-show rate %) and an alpha level Then the system enforces mutually exclusive assignments so no seat/row/session is in more than one variant simultaneously And the assignment distribution preview reflects the chosen ramp within ±2 percentage points And the experiment is saved with an experiment ID, scheduled start/stop, and an audit log entry
Control vs Treatment Dashboard Metrics & Significance
Given an active experiment has collected booking, attendance, and payment data When I open the experiment dashboard Then I see side-by-side control vs treatment metrics for revenue per seat, seat utilization %, and no-show rate % And each metric displays sample size, uplift %, 95% confidence interval, and p-value And a badge indicates Significant or Not Significant based on the selected alpha And the statistical test used is displayed (t-test for revenue; z/chi-square for rates) And metrics refresh within 15 minutes of new bookings, cancellations, or check-ins
Cohort Filters Recompute Metrics & Significance
Given an experiment spans multiple cohorts When I apply filters for class type, instructor, location, time of day, day of week, row group (front/middle/back), and customer segment (new vs returning) Then the dashboard recomputes metrics, uplift, and significance using only filtered data And the active filter state is encoded in the URL and included in CSV exports And clearing filters restores the unfiltered results
Attribution to Recommendations and Classes with Conflict Handling
Given an experiment is linked to specific recommendations and classes When I view attribution details Then each results panel lists recommendation ID and version and the affected class IDs and dates And a drill-down table shows per-class session outcomes by variant And if overlapping experiments target the same sessions, a conflict warning appears and those sessions are excluded by default with a toggle to include them
Scheduled Reports and CSV Export Respecting Permissions
Given I have Viewer or Admin access to the experiment When I schedule a report with cadence (daily/weekly), time, timezone, and recipients Then recipients receive emails at the scheduled times with a summary and CSV matching current filters and date range And manual CSV export includes columns: experiment_id, variant, metric_name, value, sample_size, uplift_pct, p_value, ci_low, ci_high, filters, generated_at And exports up to 100,000 rows complete within 10 seconds and are permission-restricted
Post-Implementation Tracking with Guardrails and Result Freezing
Given an experiment is ended and the winning recommendation is rolled out to 100% of eligible sessions When I enable post-implementation tracking with a matched baseline window Then the system freezes experiment results as an immutable snapshot with timestamp and author And a dashboard shows realized uplift vs baseline with confidence intervals and trend over time And guardrails monitor revenue per class, utilization %, and no-show rate %; if thresholds are breached, an alert is sent and the recommendation is flagged for rollback

AccessAssist

Designate accessible paths and priority seats; capture mobility needs at booking; and auto-assign suitable spots first. Instructors see clear badges on the roster, ensuring inclusive, compliant seating without last-minute reshuffles.

Requirements

Booking Accessibility Intake
"As an attendee, I want to share my mobility needs during booking so that I’m assigned a suitable spot without extra back-and-forth."
Description

Add an optional accessibility section to the booking flow to capture mobility needs and preferences (e.g., wheelchair user, limited stairs, aisle preference, companion seat, service animal, extra time to enter), plus a free-text note. Store responses at the booking level with an attendee opt-in to save as a reusable profile preference. Ensure fields are accessible, localized, and mobile-friendly; expose via API/webhooks for studio CRMs. Respect data minimization and only surface relevant details to instructors and front-desk roles. Enable per-organization toggles and default prompts, and validate inputs without blocking checkout. Synchronize captured needs with roster views and seat assignment logic in real time.

Acceptance Criteria
Optional Accessibility Intake in Booking Flow
Given a customer is booking a class on web or mobile checkout When they reach the Accessibility section Then the section is clearly marked optional and can be skipped And the customer can select any of the following options: wheelchair user, limited stairs, aisle preference, companion seat, service animal, extra time to enter And the customer can select multiple options simultaneously And the customer can add an optional free-text note up to 250 characters And proceeding to payment without interacting with this section does not block checkout or display error banners
Consent to Save Preferences to Profile
Given a signed-in customer has entered accessibility selections and/or a note When they check an explicit opt-in checkbox to save preferences to their profile (unchecked by default) Then the preferences are saved to the customer’s profile upon successful booking And future bookings prefill the Accessibility section with the saved selections and note And when the customer leaves the opt-in unchecked, no profile-level data is saved or updated for that booking And if the customer has existing saved preferences, they can modify the prefilled values for this booking without altering profile data unless the opt-in is checked
Booking-Level Storage and Role-Based Visibility
Given a booking is created or updated with accessibility data When the system persists the booking Then the accessibility selections and note are stored at the booking record level And unless opt-in was granted, no profile-level accessibility data is created or modified And in roster and booking views, instructors see standardized badges representing selections but not the free-text note And front-desk role users see both badges and the free-text note And users without instructor or front-desk roles do not see accessibility fields in UI or API
Accessibility, Localization, and Mobile Compliance
Given the Accessibility section is rendered When tested with assistive technologies, multiple locales, and small screens Then all inputs have programmatic labels and descriptions announced by screen readers And the section is fully operable via keyboard with visible focus and logical tab order And color contrast meets WCAG 2.1 AA (text contrast ratio ≥ 4.5:1) And any inline messages are announced via aria-live and are localized And all UI text is sourced from the i18n catalog and displayed in the user’s selected locale And on viewports down to 320×568, no horizontal scrolling occurs and touch targets are at least 44×44 px
Per-Organization Toggle and Default Prompts
Given an organization admin opens Accessibility Intake settings When they enable or disable the feature Then the booking flow shows or hides the Accessibility section for that organization within 1 minute And the admin can configure default prompt text and which options are presented And configured defaults appear for customers at checkout and do not affect other organizations
API and Webhook Exposure for Studio CRMs
Given a booking is created or updated with accessibility data When an authorized client retrieves the booking via API Then the payload includes accessibility_needs (array of codes), accessibility_note (string), and accessibility_consent_saved_to_profile (boolean) And the accessibility_note is returned only to clients with a scope permitting front-desk or instructor access; otherwise it is omitted And upon create/update, a booking.updated webhook is delivered within 10 seconds with the same fields, signed, and retried with exponential backoff for up to 24 hours on failure
Real-Time Sync to Roster and Seat Assignment with Non-Blocking Validation
Given a booking’s accessibility data is submitted or edited When an instructor opens or refreshes the roster or the seat assignment algorithm runs Then attendee badges reflect the latest selections within 2 seconds of change And seat assignment uses accessibility_needs to prioritize suitable seats before confirmation And when a need cannot be satisfied by available seats, the attendee is flagged with a conflict indicator and suggested alternatives are shown And if any accessibility input fails validation (e.g., note > 250 chars or unknown option code), the booking still completes; invalid values are ignored or truncated with a non-blocking inline message
Venue Layout & Seat Tagging
"As an instructor, I want to tag seats and paths as accessible so that the system can match attendees to appropriate spots automatically."
Description

Provide a layout builder to define class seating/spot maps per location and template, including rows, zones, entrances, and pathways. Allow instructors to tag seats/spots with attributes such as Accessible, Aisle, Companion, Near Entrance, Extra Space, and Priority Row, and to mark accessible paths and obstructions. Support import of a floor image for reference, template cloning, and versioning with effective dates. Validate that a configurable minimum number of accessible and companion seats are available relative to capacity. Persist layouts to classes and sessions, expose tags to the assignment engine and roster UI, and handle capacity changes without breaking existing assignments.

Acceptance Criteria
Layout Builder: Define Rows, Zones, Entrances, and Pathways
Given I am creating a new venue layout template for a location When I add rows with specified seat counts, zones with labels, one or more entrances, and pathway segments Then each element is persisted with type, label, and coordinates And rows and zones cannot overlap entrances And pathway segments snap to valid connection points and do not intersect obstructions And saving the template preserves all elements and their geometry for reload without loss
Seat/Spot Tagging: Accessible, Aisle, Companion, Near Entrance, Extra Space, Priority Row
Given a seat/spot is selected in the layout builder When I apply one or more tags from the supported set {Accessible, Aisle, Companion, Near Entrance, Extra Space, Priority Row} Then the tags are persisted on that seat/spot and visible as badges in the builder And the same tags are exposed in the roster UI for classes using this layout And the same tags are included in the assignment engine payload for auto-assignment And removing a tag updates persistence, roster UI, and assignment payload within 60 seconds
Accessible Paths and Obstructions Connectivity Validation
Given obstructions and accessible path segments are marked on the layout When I mark one or more seats/spots as Accessible Then each Accessible seat/spot must have at least one continuous accessible path to at least one entrance And seats/spots completely blocked by obstructions cannot be assigned in preview mode And if any Accessible seat/spot lacks a connected path, saving is blocked with an error listing the seat identifiers
Floor Image Import and Alignment
Given I have a PNG or JPG floor image (max 10 MB) When I upload the image to a layout template version Then the image is stored with the template version and displayed as a non-interactive background layer And I can adjust image scale (25%–200%) and opacity (0%–100%) and position it behind the layout And the reference image is not shown in attendee-facing views or rosters And replacing or removing the image updates the stored version and preview immediately
Template Cloning and Versioning with Effective Dates
Given an existing layout template version V1 When I clone the template Then a new template is created with identical geometry and tags and a new identifier Given template versions have effective start and end dates When I add a new version V2 with dates that overlap V1 Then the system prevents saving and instructs me to resolve the overlap And sessions starting on date D automatically attach the version whose effective range includes D And sessions already created retain their attached version regardless of future changes
Accessible and Companion Seat Minimums Validation
Given organization settings define minimum ratios for Accessible and Companion seats relative to capacity When I save a layout template or attach it to a class/session with capacity C Then the system calculates required counts: Accessible >= ceil(C * accessible_min_ratio) and Companion >= ceil(C * companion_min_ratio) And only seats/spots not obstructed and with a valid accessible path count toward the totals And if requirements are not met, the save/attach action is blocked with a message showing required vs available counts
Persist to Classes/Sessions and Capacity Changes Without Breaking Assignments
Given a class and its sessions are linked to a specific layout template version and attendees have assigned seats/spots When class/session capacity increases Then existing assignments remain unchanged and additional seats/spots become available for new assignments When class/session capacity decreases Then the system does not auto-remove or invalidate existing assigned seats/spots; it reduces only unassigned capacity and prompts to reassign if needed And switching the template to a different version after session creation does not change existing sessions unless explicitly updated by the instructor
Auto-Assign Priority Seats
"As a studio owner, I want the system to automatically assign suitable seats to guests with mobility needs so that I minimize manual reshuffles and ensure compliance."
Description

Implement an assignment engine that, at booking and check-in, matches attendees’ declared needs to tagged seats/spots and auto-assigns the most suitable available option. Apply deterministic rules with tie-breakers (booking time, membership tier, proximity preferences) and support instructor overrides with safeguards and warnings. Lock assignments to prevent accidental reassignment, and re-balance when cancellations occur. Provide real-time updates to attendee confirmations and instructor rosters, with notifications on assignment or changes. Fail gracefully when suitable seats are unavailable by placing users in a pending state and prompting staff to resolve. Log all assignment decisions for auditability.

Acceptance Criteria
Auto-assign at booking and check-in
Given a class with tagged accessible seats/spots and an attendee declaring mobility needs When the attendee completes booking or is checked in by staff Then the engine assigns the best matching available seat based on rules And the assignment completes within 2 seconds And the attendee confirmation displays the assigned seat/spot ID and accessibility badge And the instructor roster updates in real time within 2 seconds And an assignment notification is sent to the attendee and surfaced on the instructor dashboard And the assigned seat is marked as locked
Deterministic tie-breakers for seat selection
Given multiple seats/spots satisfy the attendee’s declared needs When the engine selects among the candidates Then the earliest booking timestamp is chosen over later ones And if timestamps tie, the higher membership tier is chosen And if tiers tie, the higher proximity preference score is chosen And if scores tie, the lowest seat/spot numeric order is chosen And the same inputs always yield the same seat selection And the applied tie-breaker path is recorded
Instructor override with safeguards and warnings
Given an instructor attempts to move an attendee to a different seat/spot When the target does not meet the attendee’s declared needs Then the system blocks the reassignment by default and displays a warning with reasons and compatible alternatives And allows override only if the instructor confirms and provides a justification And on successful override, both attendee and instructor receive change notifications And the override action, justification, and prior/target seats are logged When the target meets the needs and is available Then the reassignment succeeds and the new seat remains locked
Assignment locking and concurrency control
Given an attendee has a locked seat/spot assignment When new bookings are processed or background operations run Then the attendee’s assignment is not changed unless a cancellation affects it or the instructor explicitly unlocks/rebalances And concurrent booking attempts cannot allocate the same seat (second attempt must select a different eligible seat) And no double-bookings occur in load tests with 1000 concurrent simulated bookings And lock state transitions are recorded
Re-balance on cancellation
Given a cancellation frees a tagged accessible seat/spot And there are pending attendees whose needs match the freed seat When the engine processes the cancellation event Then the highest-priority pending attendee is auto-assigned using the same tie-breakers And unaffected confirmed assignments are not moved unless an instructor-initiated rebalance is requested And attendee confirmations and the instructor roster update within 2 seconds And notifications are sent to the reassigned attendee and surfaced to the instructor
Graceful pending state when no suitable seat exists
Given an attendee declares mobility needs and no suitable seats/spots are available When the attendee attempts to book Then the booking completes with a Pending Assignment status and no seat is assigned And the attendee sees a clear explanation and expected next steps And an alert is created for staff with a link to resolve And the attendee is placed in the waitlist according to the tie-breaker rules And the event is logged for audit
Audit logging and traceability of assignment decisions
Given any assignment, reassignment, override, rebalance, or failure-to-assign event When the event is processed Then a log entry is written with timestamp, actor (system/instructor/user), inputs (declared needs, seat tags, availability), rule path including tie-breakers, outcome (seat ID or pending), and notification references And logs are immutable and queryable by class ID and attendee ID And PII is redacted per policy while maintaining traceability And 100% of simulated events are present in the audit log during test verification
Roster Badges & Alerts
"As an instructor, I want clear badges and alerts on my roster so that I can prepare the room and avoid last-minute seat changes."
Description

Display clear, non-stigmatizing badges on rosters, check-in, and attendee detail views to indicate accommodations (e.g., Wheelchair, Aisle, Companion) and current seat assignment. Provide tooltips with limited, need-to-know context and a pre-class summary that highlights counts of accessible seats, unresolved needs, and risks. Alert instructors when manual moves would violate an attendee’s needs or reduce accessible inventory below configured thresholds, offering suggested alternatives. Support printable and offline roster modes and ensure color palettes meet contrast guidelines. Respect role-based access so only authorized staff see sensitive details.

Acceptance Criteria
Unified Badge Display with Seat Assignment
- Given an instructor opens the class roster or check-in view, When the list loads, Then each attendee with declared accommodations displays non-stigmatizing badges from the approved set (Wheelchair, Aisle, Companion) plus current seat/zone identifier. - Given an attendee has no declared accommodations, When views render, Then no badges are shown for that attendee. - Given the attendee detail view is opened, When it loads, Then the same badges and seat/zone are visible and consistent with the roster/check-in views. - Given a constrained network (<1 Mbps), When badges load, Then placeholders show immediately and final badges render within 1.0 second of list paint.
Privacy-Scoped Badge Tooltips
- Given a user with an authorized role (Owner, Instructor) hovers/taps a badge, When the tooltip opens, Then it shows only need-to-know context (<=120 characters) and compatibility notes; no diagnoses or free text appear. - Given a user with a non-authorized role (Assistant, Front Desk) interacts with a badge, When the tooltip opens, Then sensitive context is hidden and a neutral message is shown (e.g., "Accessibility note available to instructors"). - Given a screen reader user focuses a badge, When reading the tooltip, Then ARIA labeling announces the same limited context without exposing hidden fields.
Pre-Class Accessibility Summary Banner
- Given a roster is opened within 2 hours of class start, When the page renders, Then a summary banner displays: total accessible seats, accessible seats assigned, accessible seats available, unresolved needs count, and risk status. - Given any seat assignment change, When it is saved, Then the banner updates within 1 second to reflect new counts and risk status. - Given unresolved needs > 0, When the banner is shown, Then a Resolve CTA is present and navigates to the seating assistant.
Move Validation Alerts and Suggested Alternatives
- Given an instructor drags an attendee to a new seat, When the move would violate that attendee’s accommodation constraints, Then a blocking alert explains the violation and prevents the move. - Given the alert is shown, When alternatives exist, Then at least the top 3 compliant seats (or all if fewer) are suggested, sorted by proximity to the target seat. - Given no compliant alternatives exist, When the alert is shown, Then the system communicates unavailability and offers to waitlist or keep current seat.
Accessible Inventory Threshold Warnings
- Given an accessible seat threshold is configured (absolute count or percentage), When a change would reduce accessible seats below the threshold, Then a warning modal appears showing current, threshold, and projected counts and requires explicit confirmation to proceed. - Given the warning is confirmed, When the change is applied, Then the summary banner and roster header display an "At Risk" state until counts return above threshold. - Given alternatives exist that maintain the threshold, When the warning appears, Then the modal suggests compliant seat swaps to preserve inventory.
Printable and Offline Roster Support
- Given an instructor selects Print Roster, When the print preview renders, Then badges with text labels, a legend, and seat assignments are included; tooltips and sensitive notes are excluded; font size is >= 10pt; layout fits Letter and A4 without truncation. - Given the device is offline with a previously synced class, When the roster is opened, Then cached badges and seat assignments are available and marked "Offline". - Given offline status, When a seat move is attempted, Then local validation runs; if rules cannot be verified, the action is blocked with "Cannot validate offline".
Color and Contrast Compliance
- Given badges and icons are displayed, When measured, Then text contrast ratio is >= 4.5:1 and non-text/icon contrast is >= 3:1 against adjacent colors (WCAG 2.1 AA). - Given keyboard focus on a badge, When the focus ring is displayed, Then it has a minimum 2px outline with >= 3:1 contrast. - Given users toggle light and dark themes, When badges render, Then contrast ratios remain compliant in both themes.
Waitlist Accessibility Matching
"As a waitlisted attendee with mobility needs, I want to be offered accessible spots first so that I can attend without worry."
Description

Extend waitlist promotion logic to honor accessibility needs and seat tags, offering newly available accessible/priority seats first to waitlisted attendees who require them. Support configurable hold windows, multi-seat bookings with companion seats, and fallback rules when no match exists. Provide targeted notifications that indicate the seat type being offered and expiration times. Maintain transactional integrity so seats aren’t double-allocated during concurrent promotions. Expose analytics on promotion outcomes and declines to optimize accessible seat inventory.

Acceptance Criteria
Prioritized Promotion for Accessible Seats
Given a class has seat tags configured (e.g., wheelchair, aisle, low-impact) and the waitlist includes attendees with recorded accessibility needs When an accessible or priority seat becomes available Then the system identifies the subset of waitlisted attendees whose needs match the seat’s tag (exact tag or configured mapping) And offers the seat first to the highest-ranked attendee within the matching subset, preserving original waitlist order And does not offer the seat to non-matching attendees until the matching cohort is exhausted or the configured grace period lapses And the audit log records the match reason, candidate list, and offer recipient
Configurable Hold Window Enforcement
Given a promotion hold window is configured for the class or studio (e.g., 15 minutes, timezone-aware) When a promotion is sent to an attendee Then the offered seat(s) are held exclusively for that attendee for the full hold window duration And the notification displays the exact expiration timestamp and local timezone And if the attendee has not accepted and paid by expiration, the hold is automatically released and the next eligible candidate is promoted within 5 seconds And all hold start/end times and state transitions are logged
Multi-Seat Booking with Companion Seat Allocation
Given a waitlisted attendee has indicated “companion seat required” with quantity = 2 and required accessibility tag(s) When an accessible seat with compatible adjacent or linked companion seat(s) are available Then the system creates a single bundled offer holding all required seats together And the offer cannot be partially accepted; either all seats are confirmed together upon payment or all are released on expiration or decline And if compatible companion seats are not available, no offer is sent until they are, unless fallback rules explicitly allow otherwise And the bundle is priced and taxed according to configured multi-seat rules
Fallback Promotion Rules for Non-Matching Cases
Given fallback rules are configured for accessible seat inventory utilization When an accessible seat becomes available and no waitlisted attendee requires that accessibility type Then the system waits the configured accessible-hold grace period before offering that seat to the general waitlist And when a standard seat becomes available while accessibility-required attendees are waiting, the system only offers standard seats to them if they opted into “accept standard if accessible unavailable” And all fallback decisions are captured with rule IDs in the audit log
Targeted Notification Content and Timing
Given a promotion offer is generated When notifications are sent Then the email, push, or SMS includes: class details, seat type offered (including tag), number of seats, hold expiration timestamp, CTA to accept or decline, and accessibility notes And notifications are delivered within 30 seconds of offer creation for 95% of attempts And declining via any channel immediately releases the hold and triggers promotion to the next eligible candidate And notification templates support localization and accessibility guidelines (readable alt text and minimum 16pt body font in email)
Concurrency Control and Transactional Integrity
Given multiple seats become available and multiple promotions are generated concurrently When 100 concurrent promotion workflows are executed against the same class Then no seat is double-held or double-booked (oversell = 0) And each seat hold is associated with a unique immutable reservation ID and a versioned seat record to prevent race conditions And the promotion workflow is idempotent; retries do not create duplicate holds or notifications And conflicting transactions are resolved within 1 second with error telemetry recorded
Analytics Capture for Promotion Outcomes and Declines
Given promotion lifecycle events occur (offer, viewed, accepted, declined, expired, fallback-used) When analytics are queried via dashboard or API with filters for date range, class, seat tag, and outcome Then metrics are available: offers by seat type, accessibility match rate, conversion rate, median and p95 time-to-accept, decline reasons, companion-bundle uptake, and fallback utilization And analytics update within 5 minutes of event time and are accurate within ±1% And export endpoints provide CSV and JSON with schema including seat_tag, accessibility_match, hold_window_minutes, outcome, timestamps, and reservation_id
Privacy, Consent & Data Controls
"As a privacy-conscious attendee, I want control and transparency over my accessibility information so that I feel safe using the platform."
Description

Add explicit consent capture for collecting and using accessibility information, with clear explanations of purpose and retention. Allow attendees to view, edit, or withdraw accessibility preferences and to choose per-booking disclosure. Implement role-based access, field-level encryption at rest, and masked views for non-privileged roles. Provide data export and deletion to meet GDPR/CCPA requirements and configurable retention windows. Log access to sensitive fields and surface privacy settings in account, booking, and roster contexts. Ensure all notifications avoid unnecessary disclosure of sensitive details.

Acceptance Criteria
Explicit Consent for Accessibility Data at Account and Booking
Given an attendee begins entering accessibility info in account settings or during booking When the first sensitive field is focused or a value is entered Then the system displays a purpose and retention notice with an explicit unchecked consent checkbox and a link to the privacy policy Given consent is not checked When the attendee attempts to save the accessibility info Then the save is blocked and an inline error explains consent is required to store this data Given consent is checked When the attendee saves Then the system stores consent status, timestamp, policy version, context (account or booking), and user ID, and records an audit event "consent.granted" Given consent exists When the attendee views privacy settings in account or on the booking form Then the current consent status and last updated date are visible
Per-Booking Disclosure Controls and Notification Redaction
Given an attendee with saved accessibility preferences is booking a class When they reach the review step Then a toggle labeled "Disclose accessibility info to instructor for this booking" is present and defaults to the attendee’s saved preference Given the disclosure toggle is off When the booking is confirmed Then auto-assignment may use the data server-side, roster shows only a generic "Accessibility" badge without details, and emails/SMS/push contain no accessibility content Given the disclosure toggle is on and the viewer has permission When the booking is confirmed Then the roster shows an accessibility badge and required seat type (e.g., "Priority seat"), but no free-text medical details Given the disclosure toggle is off When notifications are sent to instructors, assistants, or other attendees Then no accessibility-related fields, labels, or inferences appear in subject lines, bodies, or calendar attachments
Role-Based Access Controls and Masked Views on Roster and Profiles
Rule: Only Owner and Admin roles can view full accessibility field values; Instructor can see badge and seat type only; Staff/Assistant see masked placeholders; Attendees can view and edit their own values; all others receive 403 Given a user without permission requests sensitive fields via UI or API When the request is processed Then the response masks values (e.g., "••••") in UI and returns 403 for direct field API, and an "access.denied" event is logged Given a permitted role views a roster or attendee profile When decrypting field values Then values are shown, and an "access.view" audit log with user ID, role, timestamp, resource, and reason is recorded Given direct database inspection outside application context When stored accessibility fields are examined Then values are encrypted (ciphertext), satisfying field-level encryption at rest
Self-Service View/Edit and Consent Withdrawal Effects
Given an attendee navigates to Account > Privacy & Accessibility When viewing their data Then all stored accessibility preferences are listed with edit controls and a "Withdraw consent" action Given the attendee withdraws consent When confirmation is accepted Then preferences are immediately disabled for future bookings, auto-assignment stops using them, roster badges are removed for upcoming classes, and a "consent.withdrawn" event is logged Given consent is withdrawn When retention policy applies Then data is queued for deletion or anonymization per retention window, and is no longer readable by any role except where legally required Given an attendee edits their preferences When scope of data changes Then they are prompted to reconfirm consent and the consent history records the change
Configurable Retention Windows and Automated Purge
Given an Admin opens Organization > Data Retention When configuring accessibility data retention Then they can set a window between 0 and 24 months with a default of 12 months and save the change with audit logging Given the retention window elapses and there are no active or future bookings requiring the data When the nightly purge job runs Then affected accessibility fields are deleted or irreversibly anonymized, caches invalidated, and a "data.purged" event is logged per record Given a booking exists within the retention period When evaluating purge eligibility Then data is retained until booking end plus the configured buffer (e.g., 7 days) before purge Given the Admin changes the retention window When saving Then the system recalculates purge schedules and applies the shortest applicable retention to existing records
GDPR/CCPA-Compliant Data Export for Attendees
Given an authenticated attendee requests a data export from Account > Privacy When the request is submitted Then the system generates a machine-readable export (JSON + CSV) including accessibility fields, consent history, purposes, and retention settings within 72 hours Given an export is ready When delivering to the attendee Then the archive is encrypted (password-protected) and a download link with expiry <= 7 days is sent; the action is logged as "export.delivered" Given the attendee views the export When inspecting contents Then no third-party or other users’ data is included, and field values match current stored values at export time
Access Logging and Audit Trail for Sensitive Fields
Given any read, write, export, purge, or consent action on accessibility fields When the action completes Then an immutable audit log entry is recorded with user or service principal, role, action type, resource ID, timestamp, IP, and outcome Given unusual access patterns (e.g., >10 sensitive field views by the same user in 5 minutes) When detected Then a security alert is generated for Admins without exposing sensitive field contents Given an Admin opens Organization > Audit Logs When filtering by "sensitive fields" Then results can be filtered by action type, user, date range, and exported as CSV without sensitive values
Compliance Reporting & Audit Log
"As a studio owner, I want compliance-ready reports so that I can demonstrate accessibility practices during audits."
Description

Generate exportable reports summarizing accessible seating availability, assignments, overrides, unresolved needs, and instructor actions by class, date range, and location. Include timestamped audit logs of assignment decisions, warnings shown, overrides applied, and consent statuses without exposing excess PII. Provide CSV and PDF formats with role-based access and redaction options. Surface KPIs (e.g., percentage of needs fulfilled, accessible seat utilization, incidents avoided) to monitor inclusion outcomes and support regulatory or grant reporting.

Acceptance Criteria
Filtered CSV Export: Accessibility Summary by Class/Date/Location
Given I am an Admin with reporting permissions And I select a date range, one or more locations, and one or more classes When I export the Accessibility Summary as CSV Then the file contains per-class rows with columns: Class ID, Class Name, Location, Start Time (ISO 8601, org time zone), Accessible Seats Total, Accessible Seats Reserved, Accessible Seats Available, Needs Captured, Needs Fulfilled, Unresolved Needs, Overrides Count, Instructor Actions Count And the rows include only sessions within the selected date range and locations for the selected classes And all counts match the roster data exactly And the file is named ClassTap_AccessibilitySummary_YYYYMMDD-YYYYMMDD_<scope>.csv And headers are present even when there are zero rows And the export completes within 10 seconds for up to 10,000 class sessions And no PII fields (full name, email, phone) are present unless my role allows unredacted exports
PDF Audit Report: Assignment Decisions and System Warnings
Given I select a date range, locations, and classes When I generate the Audit Log as PDF Then events are listed in chronological order with fields: Timestamp (ISO 8601 with time zone), Event Type, Actor Role, Actor Alias, Class ID, Details And supported Event Types include: Auto-Assignment, Manual Override, Warning Shown, Override Confirmed, Consent Recorded, Seat Released, Waitlist Promoted And events that change assignments include before and after seat state And the PDF includes a header with report title and filter scope and a footer with page X of Y and generated timestamp And the PDF is tagged and text-selectable And the report excludes emails and phone numbers and shows participant names as initials unless my role permits unredacted data And generation completes within 15 seconds for up to 5,000 events
Role-Based Access & Redaction Controls for Reports
Given I am an Instructor When I access reporting Then I see only classes I teach and all exports are redacted: participant identifier shown as anonymized alias, no email or phone, consent shown as status only Given I am an Owner/Admin When I export a report Then I can choose redaction level (Full, Partial, None), the default is Partial, selecting None requires MFA re-authentication and a reason, and the chosen level is applied to the file Given I am an External Auditor When I access reporting Then I can view KPI summaries and redacted audit PDFs only and CSV downloads are disabled And all report access and redaction selections are logged with timestamp, user ID, and report scope And unauthorized roles receive 403 with no file generated
Inclusion KPI Computation and Export
Given I select a date range, locations, and classes When I generate any compliance report Then the following KPIs are calculated for the scoped data and displayed in the report and included in CSV/PDF summaries: Needs Fulfilled % = (bookings with needs accommodated / bookings with needs) × 100, Accessible Seat Utilization % = (accessible seats reserved / accessible seats total) × 100, Incidents Avoided = count of warnings resolved prior to class start that resulted in fulfilled needs And KPIs are rounded to one decimal place (integers for counts) and include N/A when the denominator is zero And KPI values reconcile with detailed rows within the same export And KPIs respect all filters (date range, location, class)
Consent and Override Documentation in Audit Log
Given a manual override is applied to a seat assignment affecting accessibility When the override is confirmed Then a reason code must be selected from a controlled list and consent status (Obtained, Declined, Not Required) must be recorded with method and timestamp And the audit log records: Timestamp, Actor Role, Class ID, Affected Booking Alias, Override Applied = true, Reason Code, Consent Status, Consent Method, Consent Timestamp And CSV/PDF exports include these fields and a Noncompliant Override flag when consent was required but not recorded And free-text notes, if allowed, are validated to reject emails, phone numbers, and other PII And participant PII is not included beyond allowed role-based redaction
Data Filters, Time Zones, and Integrity Checks
Given I select an organization time zone for reporting When I filter by date range, location, and class Then the date range is inclusive of start and end dates in the chosen time zone and all timestamps are normalized to that time zone in exports And results include only records matching all selected filters and match UI totals for the same scope And sorting is stable (Class Start Time ascending, then Class ID) so that repeated exports with the same filters produce identical CSV content within the day And deleted or merged classes are excluded from exports unless explicitly included via ID selection And export download links expire after 24 hours, are single-use, and are delivered over HTTPS; files are encrypted at rest And large exports (up to 100,000 events) are processed asynchronously with a progress indicator and complete within 2 minutes

Auto-Localize

Automatically detects each viewer’s time zone and renders class times in their precise local time with a clear “Your time” label. Honors manual overrides for traveling users and keeps conversions accurate across web, email, and embeds—eliminating mental math and reducing missed sessions.

Requirements

Auto-Detect & Persist Time Zone
"As a prospective attendee, I want class times to automatically appear in my local time so that I can book confidently without doing time zone math."
Description

Automatically determine the viewer’s IANA time zone using client capabilities with server-side fallback, then persist the resolved zone per device (cookie/local storage) and per authenticated profile. Initialize detection on first load of public pages, embedded widgets, and the ClassTap dashboard; re-resolve on OS time zone changes and when users sign in. Provide resilient fallbacks (studio default → UTC) when detection fails. Centralize the resolved time zone in a shared service so all components access a single source of truth, minimizing drift and preventing double-booking due to mismatched interpretations.

Acceptance Criteria
First-Load Auto-Detection (Public & Dashboard)
Given a first-time visitor loads any public class/schedule page or an authenticated user loads the dashboard for the first time on a device When the page initializes Then the system resolves a valid IANA time zone via client APIs And sets the resolved zone as the single source of truth for the session before any class time is rendered And persists the zone to localStorage or cookie with a 90-day TTL And displays all class times labeled "Your time" in the resolved zone
Embedded Widget Auto-Detection
Given a host site loads a ClassTap embedded widget When the embed initializes on a device with or without third-party cookies Then the system resolves a valid IANA time zone via client APIs within the embed context And persists the zone in the embed’s local storage scope without relying on third‑party cookies And renders all times in the resolved zone with a "Your time" label identical to the main site for the same device/time zone
Per-Device and Per-Profile Persistence & Re-Resolve on Sign-In
Given a user with profile time zone B signs in on a device that has a persisted device zone A When authentication completes Then the resolved zone becomes B And the device persistence is updated to B And subsequent renders in web, embeds, and dashboard use B And signing out reverts the resolved zone to the device’s persisted zone if present, otherwise triggers auto-detection
Manual Override Selection & Cross-Surface Rendering
Given a user manually selects an IANA time zone C from the selector When the selection is saved Then the resolved zone becomes C immediately And the override persists to the user profile if authenticated, else to device storage And all surfaces (public pages, embeds, dashboard, transactional emails) render class times in C with a "Your time" label And the user can reset to Automatic to clear the override and trigger re-detection
OS Time Zone Change Auto Re-Resolve
Given the application is open When the device OS time zone changes Then the system detects the change and updates the resolved zone within 30 seconds without a full page reload And all displayed times re-render in the new zone And the new zone is persisted to device storage and, if authenticated without manual override, to the user profile
Fallback Resolution Order (Server and Client)
Given client-side detection is unavailable or returns an invalid/non‑IANA identifier When resolving the time zone Then the system falls back to the studio’s default time zone if set; otherwise falls back to UTC And the fallback zone is exposed by the shared time zone service and used consistently across web, embeds, and emails And no "Your time" label is displayed if using studio default or UTC fallback
Single Source of Truth & Booking Integrity
Given any component requests the resolved time zone during a session When the time zone service is queried from multiple components (UI renderer, booking form, email composer) Then the service returns the same IANA zone string for the session and user context And when a booking is created, the server computes the canonical start timestamp in UTC and converts for display using the resolved zone And the class start time shown on the booking confirmation page, dashboard, and email matches within 0 minutes across surfaces for the same booking And the API rejects booking requests where the client-submitted local start time does not map to the server’s canonical UTC time for that class
Manual Override & Travel Mode
"As a traveling attendee, I want to set a temporary time zone override so that all class times and reminders reflect where I’ll be during my trip."
Description

Offer a clear control to manually select a different time zone when traveling, with optional duration (e.g., until date or for N days) and a one-click reset to automatic detection. Display a lightweight banner that indicates the override and the selected time zone. Persist the override in the user profile when signed in and per device when anonymous, syncing across sessions. Accept tz parameters in booking and email links to pre-set the override. Ensure the override propagates through checkout, reminders, and waitlist-to-payment flows to avoid missed sessions.

Acceptance Criteria
Manual Time Zone Override Selection
Given automatic time zone detection is active, When the user opens the time zone control and searches by city/region, Then the system lists matching valid IANA time zones. Given the user selects a valid IANA time zone, When selection is confirmed, Then all visible class times render in the selected zone and display a "Your time" label. Then the override mode is set to Manual. Then daylight saving rules for the selected zone are correctly applied to all rendered times.
Travel Mode Duration and Expiry
Given a manual override is active, When the user sets a duration "until [date]" (interpreted in the selected zone), Then the override remains active through that date and expires at 23:59:59 of that date in the selected zone, reverting to automatic detection. Given a manual override is active, When the user sets a duration "for N days", Then the override expires at 23:59:59 on the Nth calendar day after the start date in the selected zone, reverting to automatic detection. Given a manual override is active with no duration set, Then it persists indefinitely until the user resets it or a duration is configured.
One-Click Reset to Automatic Detection
Given a manual override is active, When the user clicks "Reset to Automatic", Then automatic time zone detection is re-enabled and all class times re-render accordingly. Then any override banner is removed. Then any stored override (profile or device) is cleared.
Override Banner Visibility and Content
Given a manual override is active, Then a lightweight banner is visible on public booking pages, embedded widgets, and the signed-in web app, showing the selected time zone name (IANA) with UTC offset and a "Reset to Automatic" control. When the user navigates between pages, refreshes, or re-opens the site, Then the banner remains visible while the override is active. Then the banner is not displayed when no manual override is active.
Persistence and Sync for Signed-in and Anonymous Users
Given a signed-in user sets a manual override (with optional duration), Then the override (zone and expiry) is saved to the user profile and applied on subsequent sessions and devices after sign-in. Given an anonymous user sets a manual override, Then the override is saved per device and persists across sessions on that device until expiry or reset. Given a user has both a profile override and a different device override, When the user signs in, Then the most recently updated override is applied and the other store is updated to match.
tz Link Parameter Pre-sets Override
Given a landing URL includes a tz parameter with a valid IANA time zone (e.g., tz=America/Denver) or a UTC offset in the form "+/-HH:MM", When the user opens the link, Then the manual override is set to that zone with no duration and the override banner is shown. Given the tz parameter is invalid or unsupported, When the user opens the link, Then the override is not changed and no error message is shown. Then the pre-set override persists according to signed-in or anonymous persistence rules.
Propagation Through Checkout, Reminders, and Waitlist-to-Payment
Given a manual override is active, Then class times shown in checkout steps, order confirmation, and attendee dashboards render in the selected zone and include a "Your time" label. Given a manual override is active at message generation time, Then confirmation emails, reminder emails, and waitlist-to-payment emails render class times in the selected zone within the email body and include a "Your time" indicator. Given a manual override expires before a scheduled reminder is generated, Then that reminder uses the current effective time zone (automatic or new manual override) at generation time.
Cross-Channel Consistent Rendering (Web, Email, Embeds, ICS)
"As an attendee, I want class times to match across the website, emails, and my calendar so that I don’t get confused and miss sessions."
Description

Unify time rendering through a shared conversion utility that outputs formatted times in the resolved/overridden time zone for all surfaces: public booking pages, admin views, embedded schedules, transactional emails (confirmations, reminders, invoices), and calendar attachments (ICS). Ensure emails include explicit “Your time” context and that ICS events embed TZID/VTIMEZONE blocks to preserve correctness across clients. Provide API/query support (e.g., tz=America/Chicago) for partner embeds. Guarantee that the same event appears at the same local time across channels to reduce no-shows and support revenue goals.

Acceptance Criteria
Consistent Time Rendering Across Channels
Given a class scheduled at 2025-11-03T18:00:00Z And a viewer resolved to America/Chicago When the class time is rendered on public booking pages, admin views, embedded schedules, transactional emails, and ICS attachments Then each surface uses the exact same formatted time string returned by the shared conversion utility for America/Chicago And the wall‑clock time equals 12:00 PM on Nov 3, 2025 in America/Chicago on every surface And no surface differs by more than 0 minutes from another for the same event
Manual Time Zone Override Persists Cross-Channel
Given a viewer manually sets their time zone to Europe/London on the booking page When they view the embedded schedule for the same account, receive transactional emails for that booking, and add the event via the attached ICS Then all surfaces render event times in Europe/London And transactional emails include the label "Your time" near the time details And the generated ICS uses TZID=Europe/London with an appropriate VTIMEZONE block And the override remains in effect for that user/session until they clear or change it
Emails Include 'Your time' Context
Given any transactional email (confirmation, reminder, invoice) is generated for a booking And the viewer’s resolved time zone is America/Chicago When the email is sent Then each displayed class time is annotated with the label "Your time" And the email body includes the resolved IANA time zone string (e.g., America/Chicago) adjacent to the primary time details And no alternative time zone for the same event is shown that could conflict with the "Your time" display
ICS Attachments Preserve Time Zone
Given an event with start 2025-11-03T18:00:00Z and end 2025-11-03T19:00:00Z And the viewer’s resolved time zone is America/Chicago When generating the .ics attachment Then the file contains a VTIMEZONE component for America/Chicago and DTSTART/DTEND fields with TZID=America/Chicago And importing the .ics into Google Calendar, Apple Calendar, and Outlook renders the event as 12:00 PM–1:00 PM on Nov 3, 2025 local time And the event time remains correct across DST changes in those clients
Partner Embed tz Query Support
Given a partner loads the embedded schedule with the query parameter tz=America/Chicago When the embed renders event times Then all event times are converted using America/Chicago And the same events render at the same local time as on the public booking page when that page is set to America/Chicago And if tz is invalid or unsupported, the embed falls back to the viewer’s browser time zone without throwing an error
Accurate Rendering Across DST Transitions
Given events scheduled within 2 hours before and after the DST transition in America/New_York and Europe/Berlin When times are rendered on public booking pages, admin views, embedded schedules, transactional emails, and ICS attachments Then each surface shows identical local wall‑clock times with the correct UTC offset for each event And there are no off‑by‑one‑hour discrepancies between any surfaces
Time Zone Resolution Precedence
Given a viewer with browser time zone America/Los_Angeles And a schedule URL containing tz=America/Chicago And the viewer has set a manual override to Europe/London When rendering times across web, email, embeds, and ICS for that viewer Then the manual override (Europe/London) takes precedence And if the manual override is cleared, the tz query parameter (America/Chicago) is used And if no tz parameter is present, the browser time zone (America/Los_Angeles) is used
DST & Edge-Case Accuracy with IANA Time Zones
"As an instructor, I want class times converted accurately during DST changes so that attendees receive correct reminders and arrive at the right time."
Description

Base all conversions on UTC + IANA TZIDs to correctly handle daylight saving transitions, historical rules, and regional quirks. Detect ambiguous/non-existent local times during DST shifts and display a subtle note if an event falls in a transition window. Lock conversions to the event’s original time zone context to prevent instructor-side drift. Add regression tests pinned to known DST boundaries and update processes tied to IANA releases. Prevent double-booking and reminder misfires by resolving times at send time with the latest rules.

Acceptance Criteria
Ambiguous DST Fall-Back Conversion (America/New_York 01:30 occurs twice)
Given an event stored with UTC=2025-11-02T05:30:00Z and TZID=America/New_York When a viewer in America/New_York views the event time Then the time is rendered once as 1:30 AM with the correct UTC offset for that instant and a subtle “DST change” note And no duplicate listing is created for the repeated hour And viewers in other time zones see their correct local time for the same instant using their IANA rules
Non-Existent Spring-Forward Local Time Validation (Europe/Berlin 02:30 skipped)
Given an instructor selects 02:30 local time on the spring-forward date in TZID=Europe/Berlin When attempting to save the event Then the save is blocked with a message that the selected time does not exist due to DST and requires choosing a valid time And suggested valid alternatives (e.g., 02:00 or 03:00) are presented And upon selecting a valid time, the event saves with stored UTC and TZID=Europe/Berlin
Event Time Zone Locking Prevents Instructor Drift
Given an event created with start UTC=2025-06-10T17:00:00Z and event_tzid=America/Los_Angeles And the instructor later changes their account time zone to America/Denver When viewing the event on instructor and attendee surfaces (web, email, embeds) Then the event remains anchored to the original UTC instant and TZID with no shift in attendee wall time And stored UTC and event_tzid remain unchanged And reminders and conversions continue to use the event’s original TZID context
Reminder Send-Time Resolution Uses Latest IANA Rules
Given tzdata is updated between event creation and reminder send time And the platform has pulled and activated the latest IANA release When the reminder job computes send times for attendees across time zones Then send-time calculations use the latest IANA rules and align with the event’s UTC instant And no reminder is sent twice or missed due to DST transitions And the tzdata version used is recorded in logs for the send batch
IANA Release Ingestion and Regression at Known DST Boundaries
Given a new IANA tzdata release becomes available When the update pipeline runs in staging Then the release is downloaded, applied, and activated without errors And a regression suite of pinned DST-boundary cases across at least 10 regions passes 100% And the same version is promoted to production with a recorded version identifier And a scheduled job verifies no change in computed instants for existing events post-update
Double-Booking Prevention Across DST Transition Windows
Given two customers attempt to book the last seat for a class that spans a DST fall-back transition in TZID=Europe/London When their requests are processed concurrently during the repeated hour Then the system resolves class times to UTC and enforces capacity based on the true instants And at most one booking is confirmed and the other is placed on waitlist And no duplicate reminders or calendar holds are created for the repeated hour
“Your Time” Labeling & Original Time Toggle
"As a cautious attendee, I want a clear “Your time” label and an easy way to see the instructor’s original time so that I can verify I’m booking the correct slot."
Description

Visibly annotate displayed times with a non-intrusive “Your time” label and provide a tooltip or inline toggle to view the instructor’s original time zone time. Respect locale and user preference for 12/24-hour formats and provide accessible labels for screen readers. Apply consistent formatting and labeling in lists, detail pages, checkout, and emails to eliminate ambiguity. Persist the original-time toggle per user/session to support cross-page continuity.

Acceptance Criteria
List Views: Localized 'Your time' Annotation
Given an event with source time 2025-03-10 10:00 America/New_York And a viewer with resolved time zone Europe/Berlin And the viewer's time format preference is 24-hour When the viewer opens any class list or embedded list Then each event time is displayed as 15:00 with the visible label "Your time" And the time conversion matches IANA zone conversion rules for Europe/Berlin And the instructor's source time is not displayed by default And the "Your time" label has aria-label "Your time — Europe/Berlin" and is included in the accessible name for the time element And the displayed time string is not truncated on viewport widths >= 320px
Detail & Checkout: Toggle to Instructor Time
Given an event with source time 2025-03-10 10:00 America/New_York And a viewer with resolved time zone Europe/Berlin When the viewer toggles "Show instructor time" on the class detail page Then the displayed time switches to 10:00 with label "Instructor time (America/New_York)" And "Your time" is not displayed while the toggle is on And the toggle is a keyboard-focusable control with role="switch" and an accessible name "Show instructor time" And the toggle state persists across navigation to checkout and back during the same browser session And after page reload within the same session, the chosen mode remains applied
Manual Time Zone Override (Traveler Mode)
Given auto-detected time zone is America/Los_Angeles And the viewer selects a manual override to America/New_York When any page with event times is rendered Then times display in America/New_York with the "Your time" label And an info tooltip "Using manual time zone: America/New_York" is available next to the label And a "Use device time zone" control restores auto-detection when activated And the manual override persists across pages until the viewer resets it or the session ends
12/24-Hour Formatting by Locale and Preference
Given the viewer locale is en-US and no explicit time format preference is set When event times are displayed Then times use 12-hour format with AM/PM (e.g., 3:05 PM) And the "Your time" label is present Given the viewer sets a preference for 24-hour format When event times are displayed on web, embeds, and emails Then times use 24-hour format (e.g., 15:05) regardless of locale And the preference persists for the current session and for logged-in users across visits
Consistency Across Lists, Details, Checkout, Emails, and Embeds
Given an event with source time 2025-07-01 18:30 Europe/London And a viewer with resolved time zone America/Chicago and 12-hour preference When the viewer sees the event on list, detail, and checkout pages, and in confirmation emails and embeds Then all surfaces display the same localized time (12:30 PM) and the same label text "Your time" And emails include the instructor's original time inline as "Instructor time (Europe/London): 18:30" without requiring a toggle And web and embeds use the formatting template "{localized time} Your time"; emails use "{localized time} Your time; Instructor time ({source zone}): {source time}"
DST Transition Accuracy and Edge Cases
Given an event scheduled at 2025-03-30 10:00 Europe/London (DST start) And a viewer in America/New_York When the time is displayed Then it converts to 05:00 and is labeled "Your time" Given an event scheduled at 2025-11-02 01:30 America/New_York (ambiguous time during DST end) When displayed to a viewer in Europe/Berlin Then the time reflects the correct UTC offset for the event instance and converts to 06:30 And both instances of the ambiguous time are uniquely identifiable by their UTC offsets in internal data And no "Invalid Date" error or duplicate display occurs
Screen Reader and Keyboard Accessibility
Given a screen reader user on Windows (NVDA or JAWS) and macOS (VoiceOver) When focusing the time element in list, detail, checkout, or embed Then the accessible name announces "Your time, {localized time}, {resolved time zone name}" And when the instructor time toggle is present, it is reachable via Tab, has role="switch", announces its on/off state, and is operable via Space and Enter And interactive tooltips are associated via aria-describedby and are dismissible with Escape And color contrast for labels meets WCAG 2.1 AA (>= 4.5:1 for normal text)
Performance, Caching, and No-Flash Rendering
"As a site visitor, I want times to appear correctly and instantly so that the page feels reliable and I’m not distracted by flicker or changes."
Description

Resolve and apply the time zone within 100ms on first paint to avoid a flash of unlocalized content. Hydrate server-rendered pages with a placeholder time zone and reconcile client detection without visual jumps. Cache resolved zones per device and throttle re-computation. For embeds, support CDN caching that varies by a lightweight time zone key. Instrument performance metrics and error logs for detection failures and mismatches.

Acceptance Criteria
First Paint TZ Resolution ≤100ms, No Flash
Given a first-time visitor with no cached time zone When the page first paints Then a time zone is resolved and applied within 100 ms of first paint And no DOM element displays an unlocalized time at any moment And cumulative layout shift attributable to time zone reconciliation is ≤ 0.01 And a "Your time" label is present adjacent to localized times And performance metric tz_resolve_first_paint_ms is recorded with the measured duration
SSR Hydration Reconciliation Without Visual Jumps
Given server-rendered times use a placeholder time zone When client-side time zone detection completes Then all displayed times update to the detected time zone in a single DOM update per time element And no duplicate or flicker of time content is visible And cumulative layout shift caused by reconciliation is ≤ 0.01 And metrics tz_reconcile_ms and tz_reconcile_source are recorded
Per-Device Time Zone Cache and Throttled Re-Compute
Given a visitor returns on the same device/browser without changing OS time zone and without manual override When any ClassTap page or embed is loaded Then the cached time zone is used and no new detection call is executed And within a single browsing session, time zone detection executes at most once unless the OS time zone changes or a manual override is set And when the OS time zone changes or a manual override is set, the cache is refreshed on the next view And metric tz_cache_hit is recorded on cache hits
Manual Time Zone Override Persists and Renders at First Paint
Given a user sets a manual time zone override When the user refreshes, navigates to another page, or loads an embed Then all class times render in the overridden time zone at first paint And auto-detection does not supersede the override And the override persists across pages and embeds on the device until the user clears it And metric tz_override_active is recorded for sessions with an active override
Embed CDN Caching Varies by Lightweight TZ Key
Given an embed is served via CDN with a time zone cache key When two users in different time zones request the same embed URL Then the CDN returns distinct cached responses keyed by the time zone And subsequent requests with the same time zone key result in a cache HIT And response caching varies only by the time zone key (no user-specific identifiers) And changing the time zone key changes the localized times and cache object
Instrumentation and Error Handling for Detection Failures and Mismatches
Given client-side time zone detection fails or times out When the page renders Then the page uses the server-rendered placeholder time zone without uncaught errors And an error event tz_detection_failure is logged with context (reason, source) And performance metric tz_resolve_first_paint_ms is recorded with status=failed And when the detected time zone differs from the placeholder, a tz_mismatch event is logged including both zones
Privacy & Compliance for Time Zone Data
"As a privacy-conscious user, I want my time zone respected without revealing my location so that I can use the platform confidently."
Description

Store only the selected IANA time zone identifier and necessary timestamps—no precise location, IP geolocation details, or GPS coordinates. Document purposes and retention in the privacy policy, honor user deletion requests, and gate optional geolocation behind explicit consent where applicable. Ensure all processing aligns with GDPR/CCPA and internal data minimization standards while still delivering accurate conversions across ClassTap surfaces.

Acceptance Criteria
Data Minimization in Storage and Logs
- Only the IANA time zone identifier (e.g., "America/Los_Angeles") and necessary UTC timestamps are stored in user, attendee, and booking records. - No precise location (GPS coordinates), IP geolocation details, city/region/country fields, or raw IP-to-location mappings are persisted anywhere (DB, logs, analytics, data lake). - Application logs and analytics events include at most the IANA time zone identifier and timestamps; automated log scrubbing rejects restricted fields. - CI/CD schema and event-contract checks fail builds that introduce restricted location fields. - A 24-hour production sample of persisted data and logs contains zero occurrences of restricted location attributes.
Consent-Gated Optional Geolocation
- Any optional geolocation capability is disabled by default and is invoked only after explicit, granular user consent appropriate to the user’s region (GDPR/CCPA). - No browser/OS geolocation API calls occur prior to consent; verified via permissions prompts and network traces. - Upon consent, only the derived IANA time zone identifier is stored; raw coordinates or place names are neither stored nor transmitted to third parties. - Users can revoke consent at any time; subsequent geolocation calls cease immediately and the system reverts to manual selection or last-known IANA zone. - Consent capture, updates, and revocation are timestamped and auditable.
User-Initiated Deletion Request (GDPR/CCPA)
- A self-serve and support-assisted path exists for submitting a deletion request covering time zone data. - After request verification, all stored IANA time zone identifiers and related preference records are deleted or irreversibly anonymized in primary systems within 30 days (GDPR) and within 45 days max (CCPA). - Deletion propagates to caches, indexes, and analytics stores within 7 days; job completion is logged and auditable. - Backups are not rehydrated; affected data expires per documented backup retention (≤30 days) and is not restored to active systems. - Confirmation is provided to the requester with timestamp and scope of deletion.
Retention and Purpose Limitation Documentation
- Privacy policy explicitly lists collected fields (IANA time zone identifier and necessary timestamps only), purposes (time conversion), legal bases, and retention periods; published prior to feature release. - Internal data inventory and records of processing are updated with field names, storage locations, processors, and retention durations. - In-product disclosure accompanies the "Your time" label with a link to the privacy policy; no location details are displayed. - A documented annual review of policy and data inventory is scheduled and tracked with the latest completion date recorded.
Cross-Surface Accuracy with Minimal Data
- Web app, emails, calendar exports, and embeds render times using only stored IANA time zone identifiers and UTC timestamps; no location lookup services are called. - Automated tests cover at least 50 diverse IANA zones, including DST transitions and non-integer offsets (30/45-minute zones), with 0 conversion errors. - Emails include a clear "Your time" label and fall back to event time zone with explicit labeling if user time zone is absent. - Synthetic monitoring validates conversion consistency across surfaces for the next 60 days of events with 0-minute deviation. - Manual override by traveling users updates renderings across all surfaces within 5 minutes without storing precise location data.
Vendor and Processor Compliance Controls
- All processors/libraries involved in time detection or delivery are reviewed; DPAs (and SCCs where applicable) executed and stored. - Vendor configurations and documentation confirm no collection or storage of precise location or IP geolocation details; only IANA time zone identifiers and timestamps allowed. - Telemetry to vendors excludes restricted location attributes; event contracts enforce this and are validated in pre-production. - Quarterly vendor compliance reviews show zero nonconformities; any findings are remediated within 30 days and tracked.
Access Control and Auditability for Time Zone Data
- Least-privilege access is enforced so only required services/roles can read or write the time zone field. - Field-level access to time zone data is logged (read/write) with actor, purpose, and timestamp; logs retained per policy. - Data exports, reports, and BI datasets exclude the time zone field by default and require justification to include it. - Security tests confirm no unauthorized access paths (e.g., GraphQL/REST overfetching) can expose additional location data.

DST Shield

Prevents one‑hour surprises by detecting sessions that cross daylight‑saving changes in any region. Warns hosts at scheduling, pins reminders to the absolute start time, adjusts recurring series correctly, and sends pre‑change nudges so nobody shows up early or late.

Requirements

DST Conflict Detection at Scheduling
"As a host, I want to be warned when my class crosses a daylight-saving change so that I can confirm the intended start times and avoid attendee confusion."
Description

Detects when a single session or any occurrence of a recurring series intersects a daylight-saving transition in the event’s timezone (IANA) and any attendee timezones if applicable. Surfaces inline warnings during creation/editing, previews pre- and post-change local start times, and persists DST-impact metadata (affected dates, event timezone, anchoring mode). Provides API flags for integrators, and feeds downstream services (reminders, calendar sync, double-booking checks) to prevent one-hour surprises and reschedules.

Acceptance Criteria
Inline Warning for Single Session Crossing DST in Event Timezone
Given I select event timezone "America/New_York" and set start 2025-03-09 01:30 with duration 90 minutes When I blur the time or duration field or attempt to save Then an inline warning appears stating the session crosses a daylight saving time change and shows the transition instant Given a DST crossing is detected When the warning is displayed Then a preview shows the event’s local start/end times before and after the change and the exact UTC instants Given the warning is displayed When I adjust the start time so the session no longer intersects the transition Then the warning and preview disappear immediately without page reload Given I enter a nonexistent local time (e.g., "America/Los_Angeles" 2025-03-09 02:15) When I blur the time field Then a validation error explains the time does not exist due to DST and suggests nearest valid options
Recurring Series DST Detection and Occurrence Preview
Given I create a weekly series in "Europe/London" starting 2025-03-02 09:00 for 8 weeks, duration 60 minutes When the system evaluates the schedule Then any occurrence intersecting the DST change is flagged and the total count of affected occurrences is displayed Given affected occurrences exist When I expand DST details Then I see a per-occurrence preview with original wall-clock start time, post-change local start time if anchored to UTC, and an "Affected" label Given default anchoring mode is wall-clock When I switch anchoring mode to "UTC" before saving Then the preview updates to show local start times shifting by one hour after the transition Given an affected occurrence exists When I save the series Then the affected occurrence IDs and anchoring mode are persisted and retrievable via API
Attendee Timezone DST Impact Detection
Given my event timezone is "America/Los_Angeles" and I add an invitee in "Australia/Sydney" When a series occurrence coincides with a DST end in Australia/Sydney but not in the event timezone Then an inline note indicates attendee timezones shift on this date and lists the timezone and local start time delta for the attendee Given attendees have known IANA timezones When no attendee timezone is affected by a DST change for the scheduled occurrence(s) Then no attendee DST warning is shown Given affected attendee timezones are detected When I save the event Then the persisted metadata includes the set of affected timezones per occurrence
Persist DST-Impact Metadata on Save
Given a single session or series with any DST impact When I click Save Then the backend persists dstImpact=true with fields: eventTimezone (IANA), anchoringMode, durationMinutes, affectedOccurrenceIds, crossingType, preChangeOffsetMinutes, postChangeOffsetMinutes, and transitionInstant (UTC) Given the event is saved When I fetch the event via API Then the dstImpact fields match the saved values and affectedOccurrenceIds reference existing occurrence IDs Given the event is edited to remove DST impact When I save changes Then dstImpact is set to false and affectedOccurrenceIds is empty
Expose DST Flags in API for Integrators
Given an event or series with dstImpact=true When I call GET /v1/events/{id} or GET /v1/series/{id} Then the payload includes dstAffected (boolean), anchoringMode, eventTimezone, affectedOccurrenceIds, crossingType, and transitionSummary, and the ETag changes when any of these values change Given I request iCalendar export for a series When the series crosses a DST change Then the ICS includes VTIMEZONE for the event timezone and DTSTART/DTEND with TZID and correct RRULE/RDATE/EXDATE so external calendars show correct local times pre- and post-change
Feed Downstream Services to Prevent One-Hour Surprises
Given dstImpact=true for any occurrence When reminders are enqueued Then each reminder payload includes the absolute start instant (epoch milliseconds) and TZID to ensure notifications fire at the correct local wall-clock time before and after DST Given dstImpact=true for any occurrence When double-booking checks run Then conflicts are evaluated using absolute start/end instants so results are correct across the DST transition Given dstImpact=true for any occurrence When calendar sync runs Then the created external events reflect the appropriate local time across the transition with no duplicate or skipped occurrences
Local-Time Anchored Recurrence Rules
"As a host, I want my weekly class to stay at the same local time across DST switches so that I don’t have to fix the schedule twice a year."
Description

Generates recurring class instances that remain at the same local wall-clock time across DST changes by default, using RRULEs bound to the host’s selected timezone with explicit anchoring metadata. Supports an override to anchor by absolute UTC time for global livestreams. Re-materializes future instances on tzdata updates, and ensures capacity and conflict detection operate on the computed instance times. Minimizes manual edits while preserving host intent.

Acceptance Criteria
Weekly Class Stays at 6:00 PM Local Across DST
Given a host selects a timezone with an upcoming DST change and creates a weekly class at 18:00 local with default anchoring When the system generates occurrences spanning at least one DST transition Then each occurrence's local_start_time equals 18:00 in the selected timezone And the UTC offset updates appropriately before and after the transition And no occurrence is skipped or duplicated due to the DST change And the series can be saved without requiring manual instance edits
UTC-Anchored Livestream Series Maintains Absolute Time
Given the host enables Anchor by UTC and sets a weekly livestream to start at a specific start_time_utc When occurrences are generated across a DST transition in the host's timezone Then each occurrence's start_time_utc remains constant across all dates And the local_start_time shifts by exactly the DST delta relative to the host's timezone And the series is labeled UTC anchored in both UI and API And calendar exports include anchoring metadata indicating UTC anchoring
Handle Nonexistent/Ambiguous Times at DST Boundaries
Given a local-time anchored series produces an occurrence at a nonexistent local time due to spring-forward When generating that occurrence Then the start time is shifted forward to the first valid local time after the DST gap And instance metadata records time_adjustment = DST_FORWARD Given a local-time anchored series produces an occurrence at an ambiguous local time due to fall-back When generating that occurrence Then the earlier valid offset is selected (earliest instant) And instance metadata records ambiguous_resolved = earlier_offset
Re-materialize Future Instances on tzdata Update Without Losing Exceptions
Given the system ingests a tzdata update that changes offset rules for the host's selected timezone When re-materializing occurrences for affected series Then only occurrences with start_time >= now are recomputed And user-created exceptions (edited instances, skips, cancellations) remain unchanged And a change log records before/after offsets and times for each affected date And the host receives a notification summarizing impacted series and dates And repeated runs on the same tzdata version make no further changes (idempotent)
Conflict and Capacity Use Computed Instance Times
Given two series tied to the same resource (instructor, room, or account) have computed occurrences that overlap across a DST boundary When saving the second series or after re-materialization from a tzdata update Then the system reports a conflict for each overlapping occurrence with exact start/end timestamps And prevents double-booking And per-occurrence capacity, attendance, and waitlist promotions use the recomputed start/end times And conflict checks are re-run automatically after any re-materialization
RRULE and Metadata Persist Anchoring Intent
Given a series is created with default local-time anchoring When the series is persisted and exported Then the RRULE includes TZID equal to the selected IANA timezone And anchoring metadata anchor = LOCAL_TIME and tz_source = IANA are stored and exposed via API/exports Given a series is created with UTC anchoring When the series is persisted and exported Then anchoring metadata anchor = UTC is stored And exports omit TZID in RRULE and include explicit UTC times
Minimize Manual Edits While Preserving Host Intent
Given a local-time anchored series spans a DST change When the host previews generated occurrences Then no manual edits are required to keep the class at the intended wall-clock time And if the host updates the series start time, only future non-exception occurrences are updated And existing per-instance edits and cancellations remain intact And bulk edit and regenerate actions do not create duplicate or orphaned instances
Absolute-Time Reminder Pinning
"As an attendee, I want reminders that show my exact local start time even around DST so that I arrive at the right moment."
Description

All confirmations and reminders (email, SMS, push) are generated from persisted UTC instance datetimes and rendered per recipient in their local timezone with clear formatting (e.g., ISO 8601 and timezone abbreviation). Ensures attendees see the precise start time that accounts for DST, regardless of device settings. ICS attachments and deep links reference the same absolute time to keep messaging, calendars, and the app aligned.

Acceptance Criteria
Server-Side UTC Pinning for Outbound Notifications
Given a class instance persisted with start=S_utc and end=E_utc in UTC And a recipient with any device timezone setting When the system generates a confirmation or reminder Then the displayed start/end times are derived exclusively from S_utc/E_utc on the server And the outbound payload includes the canonical UTC instant in ISO 8601 Z (e.g., startIsoZ) And toggling the recipient device timezone does not change the delivered message content
Per-Recipient Local Time Rendering (TZ Abbrev + ISO 8601)
Given multiple recipients in different IANA timezones When a confirmation or reminder is sent Then each recipient’s message shows the local date and time computed from S_utc for that recipient’s timezone And includes the timezone abbreviation and numeric UTC offset in the text (e.g., "PDT (UTC-7)") And includes the ISO 8601 UTC instant in parentheses (e.g., 2026-03-13T02:00:00Z) And the message contains exactly one local time representation and one ISO UTC representation to avoid ambiguity
Cross-Channel Time Consistency (Email/SMS/Push/App)
Given a scheduled class instance with S_utc When email, SMS, push notifications, and an in-app deep link are produced for the same recipient Then all channels include the same ISO 8601 UTC instant S_utc And all channels display the same local time string for that recipient’s timezone And following the deep link opens the app to a view that shows the same absolute start time (matches S_utc) and the same local time string
ICS Attachment Uses Exact Absolute Start Time
Given an email confirmation with an ICS attachment for a class instance (S_utc) When the ICS file is inspected Then DTSTART equals S_utc (as Z time or TZID-mapped instant equivalent) and DTEND equals E_utc And the ICS contains a stable UID and a DESCRIPTION or URL field that deep-links to the same instance And importing the ICS into Google Calendar, Apple Calendar, and Outlook displays the event at the correct local time for the calendar’s timezone with no ±1 hour drift around DST
DST Boundary Reminder Accuracy
Given a recipient whose timezone undergoes a DST shift between reminder scheduling and event start And a class instance whose local start occurs within 48 hours of the shift or crosses the shift When reminders are sent at configured offsets (e.g., 24h, 1h before) Then the reminders display the post-shift correct local start time for the recipient’s timezone And the ISO 8601 UTC instant remains constant and matches S_utc And no reminder is off by ±1 hour relative to the true local start time
Non-Standard Timezones (±30/±45 min) Rendering
Given recipients in timezones with non-integer offsets (e.g., UTC+05:30, UTC+05:45, UTC-03:30) When confirmations and reminders are generated Then the rendered local times include the correct minutes offset and timezone abbreviation for each recipient And the ISO 8601 UTC instant matches S_utc And no rounding to the nearest hour occurs
Pre-Change DST Nudge Campaign
"As a host, I want automatic pre-DST notices sent to my class and me so that everyone is prepared and shows up on time."
Description

Proactively scans for upcoming DST transitions within a configurable window and sends targeted notifications to hosts and registered attendees for affected sessions. Includes per-session adjusted local times, clear impact explanations, and actions (acknowledge, reschedule, cancel). Hosts receive a consolidated digest; attendees receive session-level nudges. Optional requirement for attendee re-confirmation on high-impact changes to reduce no-shows and support tickets.

Acceptance Criteria
Configurable DST Transition Scan and Affected Session Identification
Given scan_window_days = 14 and a nightly job running at 02:00 UTC When a DST offset change exists in any timezone used by sessions or registered attendees within the next 14 days Then the system flags as affected any session where the session timezone OR any registered attendee's timezone changes offset between now and session.start_at And it persists a campaign record per session including: session_id, transition_time, impacted_timezones, offset_deltas, attendee_count And it excludes sessions with start_at <= now OR with zero registered attendees
Host Consolidated Digest Composition and Delivery
Given a host has one or more affected sessions When the digest send window opens at 08:00 host_local (configurable) Then the host receives exactly one consolidated digest for the day per channel preference And each session entry includes: session_title, original_local_time, adjusted_local_time, pre/post timezone abbreviations and offsets, attendee_count, action links (Acknowledge, Reschedule, Cancel) And sessions already acknowledged, rescheduled, or cancelled are omitted And delivery is idempotent within 24 hours (no duplicate digests)
Attendee Session-Level Nudge Content and Delivery
Given an attendee is registered for an affected session and has not acknowledged When nudge_time = transition_time - 48h (configurable) Then send a session-level notification via the attendee’s preferred channel And the message includes the adjusted local start time, a plain-language impact explanation referencing the offset change, and action buttons (Acknowledge, Reschedule request, Cancel) And include a deep link to the session and an updated ICS attachment with the correct absolute start time And limit to one nudge per attendee per session per transition, suppressing if already acknowledged
Recurring Series Handling Across DST Change
Given a recurring series that spans a DST transition When the campaign evaluates occurrences Then each occurrence is evaluated individually for DST impact And only occurrences affected by the offset change are included in nudges/digests And the series rule (repeat_at_local_time vs repeat_at_utc) is respected and reflected in adjusted times And unaffected future occurrences remain unchanged and are not notified
Optional Attendee Re-Confirmation for High-Impact Changes
Given Require Re-Confirmation is enabled for offset_delta >= 60 minutes and reconfirmation_cutoff = 24h before session.start_at When affected attendees are notified Then the notification requires an explicit Yes (Confirm) or No (Decline) response And attendees not responding by the cutoff are marked Unconfirmed And host digests display Unconfirmed counts per session And if auto_release_waitlist = true, unconfirmed seats are released to the waitlist at cutoff and affected parties are notified And all responses are recorded with timestamp and channel in an audit log
Action Handling for Acknowledge, Reschedule, and Cancel
Given a recipient clicks Acknowledge When the action is processed Then mark the recipient-session as acknowledged, suppress further nudges for that transition, and record the action in the audit log Given a host clicks Reschedule from the digest When a new start time is saved Then update the session’s absolute time, notify all recipients with updated details and ICS, recalculate DST impact, and close prior campaign entries for the old time Given a recipient clicks Cancel When cancellation policy allows Then release the seat, apply refund/credit rules if applicable, notify host and attendee, and update campaign and attendance metrics
Delivery Controls, Throttling, and Failure Handling
Given user channel preferences and quiet hours When DST nudges are queued and sent Then honor opt-in status and quiet hours (deferring send to the next allowed window if needed) And enforce rate limiting to a maximum of 1 DST nudge per user per 6 hours across channels And deduplicate per recipient-session per transition across email/SMS/push And retry transient delivery failures up to 3 times with exponential backoff and log permanent failures to the host dashboard
Calendar Sync and ICS DST Fidelity
"As an attendee, I want my synced calendar to reflect the correct class time across DST so that I don’t miss or double-book."
Description

Calendar integrations (Google, Apple, Outlook, ICS feeds) encode timezone-aware start/end times with VTIMEZONE components and correct RRULEs so occurrences render correctly across DST changes. Updates maintain UID stability and sequence increments. Offers per-attendee ICS where needed for virtual classes. Handles non-existent and ambiguous times by adjusting and notifying to maintain alignment between ClassTap and external calendars.

Acceptance Criteria
Weekly series across DST renders at consistent local time in external calendars
Given a weekly class scheduled at 18:00 in an IANA timezone that undergoes a DST transition When ClassTap generates the ICS (feed or attachment) for the series Then VEVENT includes DTSTART/DTEND with TZID set to that IANA zone and VCALENDAR includes a VTIMEZONE with STANDARD and DAYLIGHT rules covering the event dates And VEVENT uses RRULE:FREQ=WEEKLY to represent the recurrence And when imported/subscribed in Google Calendar, Apple Calendar, and Outlook, the occurrences before and after the transition display at 18:00 local time on their respective dates And no duplicate or missing occurrences appear across the transition
UID stability and SEQUENCE increments on series metadata updates
Given an existing published recurring event with a stable UID in the ICS feed When the host changes only series metadata (e.g., title, description, location) without altering start/end times Then the VEVENT UID remains unchanged for the master and its instances And the SEQUENCE value is incremented by 1 compared to the prior published version And subscribed Google, Apple, and Outlook calendars update the same event without creating duplicates
Single-occurrence override across DST uses RECURRENCE-ID correctly
Given a recurring series that spans a DST change And a specific occurrence that falls on or near the DST transition date When the host modifies only that single occurrence (time change or cancellation) Then the ICS publishes a VEVENT with RECURRENCE-ID set to the occurrence’s local date-time and TZID matching the master And that overridden/cancelled occurrence has its own SEQUENCE increment independent of the master And external calendars apply the change only to that occurrence; all other instances remain unaffected
Spring-forward non-existent time resolution and notification
Given the host schedules a class at a local time that does not exist due to spring-forward (e.g., 02:15 in a zone where clocks jump to 03:00) When the host saves the class Then ClassTap resolves the start to the earliest valid local time on that date (e.g., 03:00) and preserves the original duration And the host sees a confirmation indicating the adjustment And generated ICS uses the resolved local time in DTSTART/DTEND with TZID and includes the corresponding VTIMEZONE And attendees receive a notification reflecting the adjusted start time And external calendars display the resolved time without further shifts
Fall-back ambiguous time disambiguation and persistence
Given the host schedules a class at a local time that occurs twice on fall-back (e.g., 01:30 on the transition date) When the host saves the class Then ClassTap pins the event to the first occurrence (pre-change offset) by default and records this choice And the host can explicitly choose the second occurrence; the chosen disambiguation persists on subsequent updates And the ICS encodes the intended instant by converting to UTC consistently with the stored offset while keeping DTSTART with TZID and VTIMEZONE And external calendars display the event at the intended wall-time instance with the correct offset
Per-attendee ICS with personalized join link and stable updates
Given a virtual class that requires individualized join links When ClassTap sends calendar attachments/invites to attendees Then each attendee receives an ICS containing only their personalized join link (in URL and/or DESCRIPTION) And each attendee’s ICS has a unique, stable UID scoped to that attendee and event And subsequent updates to that attendee’s invite increment SEQUENCE and are recognized by Google, Apple, and Outlook as updates (not new events) And the ICS includes VTIMEZONE so the event renders at the correct local time across DST for each attendee
Cancellation semantics for series and instances with DST coverage
Given a recurring series with occurrences spanning a DST transition When the host cancels the entire series or a single occurrence Then the ICS publishes VEVENT entries with STATUS:CANCELLED and the original UID (and RECURRENCE-ID for single-instance cancellations) And SEQUENCE is incremented for the cancelled item(s) And subscribed Google, Apple, and Outlook calendars remove the corresponding series/occurrence without leaving residual or duplicate events
Timezone and DST Data Service
"As a platform admin, I want reliable, up-to-date DST rules powering schedules so that classes remain accurate without manual intervention."
Description

Maintains a centralized, versioned tzdata source with automated updates, monitoring, and rollback. Exposes APIs for timezone lookup, DST transition queries, and occurrence generation. Triggers re-computation of future instances affected by tzdata changes. Covers regions without DST and non-hour offsets to ensure global correctness and platform reliability.

Acceptance Criteria
Automated tzdata Ingestion and Versioning
Given a new upstream tzdata release is available When the scheduled update job runs Then the service downloads the release over HTTPS, verifies signature or SHA256 checksum, and stores it as a new immutable version record within 30 minutes of detection Given a version is stored When requesting GET /tzdata/version/current Then the endpoint returns HTTP 200 with version_id, source, checksum, and published_at fields Given no new release is available When the update job runs Then the service performs no version change and records an auditable no-op run entry with job_id and checked_at timestamp Given a download or verification failure occurs When the update job runs Then the service marks the run failed, emits event tzdata.ingest.failed with error details, and raises an alert within 5 minutes
Safe tzdata Version Rollback
Given an operator issues POST /tzdata/version/rollback targeting the immediately previous version When the rollback is executed Then the switch occurs atomically with under 1 second read unavailability and emits tzdata.version.rolled_back with from_version and to_version Given the rollback completes When querying any read API during and after the operation Then responses are consistent (no mixed-version results) and /tzdata/version/current returns the rolled-back version_id Given audit requirements When a rollback occurs Then an audit record is persisted with actor_id, reason, timestamp, from_version, to_version Given an invalid rollback target is requested When the request is processed Then the API responds HTTP 400 and no version change occurs
Timezone Lookup API Correctness and Latency
Given GET /timezones/{iana_id} When iana_id is a valid canonical ID Then respond HTTP 200 with fields: id, canonical=true, standard_offset_minutes, dst_observed, display_name Given GET /timezones/{iana_id} When iana_id is an alias (e.g., US/Eastern) Then respond HTTP 200 with canonical=false and canonical_id set to the canonical zone Given GET /timezones/{iana_id} When iana_id is unknown Then respond HTTP 404 with error_code=TIMEZONE_NOT_FOUND Given normal load of 500 RPS When serving GET /timezones/{iana_id} Then p95 latency <= 100 ms and 99.9% success over a rolling 30-day window
DST Transition Query Accuracy
Given GET /timezones/{id}/transitions?start=ISO8601&end=ISO8601 When the ID is valid and the range is <= 10 years Then respond HTTP 200 with ordered transitions including utc_instant, offset_before_minutes, offset_after_minutes, delta_minutes, abbreviation_after Given a region without DST When requesting transitions across a 10-year window Then respond HTTP 200 with an empty transitions array Given a zone with non-hour offset changes (e.g., Australia/Lord_Howe) When requesting the transition covering the change Then delta_minutes reflects the 30-minute shift and utc_instant matches the authoritative tzdata version in use Given an invalid date window (end before start or span > 20 years) When the request is processed Then respond HTTP 400 with error_code=INVALID_RANGE
Occurrence Generation Across DST and Non-hour Offsets
Given POST /occurrences/generate with inputs: timezone, start_local, rrule, mode=wall, ambiguous_policy in {pre,post,error} When the timezone observes DST Then generated occurrences keep the same local wall-clock time-of-day; for non-existent times at spring-forward, pre selects the last valid instant before the gap, post selects the first valid instant after the gap, error returns HTTP 422 with details Given POST /occurrences/generate with inputs: timezone, start_local, rrule, mode=wall, ambiguous_policy in {pre,post} When the local time is ambiguous at fall-back Then pre selects the earlier instant (before transition) and post selects the later instant (after transition) Given POST /occurrences/generate with inputs: start_instant_utc, rrule, mode=absolute When generating occurrences across any DST transitions Then each occurrence utc_instant follows the recurrence rule independent of offset changes; local times may vary and are returned with offset_minutes Given determinism requirements When calling the API twice with identical inputs and tzdata version Then the outputs (utc_instant and local time representations) are identical byte-for-byte
Triggered Re-computation on tzdata Change
Given a tzdata version change occurs via ingest or rollback When the change is committed Then the service emits tzdata.changed with from_version and to_version and enqueues recomputation for all future occurrences within the next 400 days Given recomputation jobs are running When schedules are evaluated Then only schedules whose offsets or occurrence instants change are updated; unaffected schedules remain unchanged; a summary report includes counts of evaluated, updated, and skipped Given idempotency guarantees When tzdata.changed is delivered more than once for the same version pair Then each schedule is recomputed at most once per schedule-version pair Given operational SLAs When recomputation starts Then 95% complete within 30 minutes and failures are retried with exponential backoff up to 5 attempts
Global Coverage and Conformance Validation
Given an automated conformance test suite When executed nightly Then at least 100 representative timezones are validated, including >=20 with non-hour offsets and >=40 with no DST, across a 5-year horizon with 100% test pass rate Given production canaries When daily cross-checks compute next 365 days of transitions for a rotating sample of 25 zones Then any deviation from the current tzdata-derived results creates a P1 alert within 10 minutes with zone, expected vs actual, and version metadata Given documentation requirements When the service is deployed Then public API docs include request/response schemas, examples for DST gaps and overlaps, and clearly state supported IANA version and update cadence
Ambiguous/Invalid Time Resolution UI
"As a host, I want the system to help me resolve invalid or duplicated times during DST changes so that my classes are scheduled correctly."
Description

When a chosen local time falls into a DST gap or overlap, the UI blocks publish and guides the host to resolve it: choose first/second occurrence for overlaps, adjust to nearest valid time for gaps, or switch anchoring mode. Provides a preview of resulting times for all affected occurrences and logs the decision for auditability. Prevents silent mis-scheduling at DST boundaries.

Acceptance Criteria
Resolve DST Overlap by Choosing Occurrence
Given the host selects a local start time that occurs twice due to a DST fall-back in the chosen timezone When the time is selected or a recurring series is generated Then the UI presents an overlap prompt with two explicit options: First occurrence (pre-shift offset) and Second occurrence (post-shift offset), each showing its UTC offset And Publish/Save remains disabled until one option is confirmed And selecting an option updates a preview showing absolute start time(s) and offsets for all affected occurrence(s) And the chosen occurrence is persisted on save and restored on subsequent edit
Resolve DST Gap by Adjusting to Nearest Valid Time
Given the host selects a local start time that does not exist due to a DST spring-forward gap in the chosen timezone When the invalid time is detected Then the UI blocks Publish/Save and displays resolution options: Move earlier to last valid minute and Move later to next valid minute (default: move later) And the UI displays the exact adjusted start time and UTC offset for each option And selecting an option updates the preview and enables Publish/Save And the applied adjustment is saved and re-applied on edit
Resolve Ambiguity by Switching Anchoring Mode
Given an overlap or gap is detected for a session or series When the host toggles anchoring between Local wall-clock and Absolute UTC Then the preview recalculates all occurrence times accordingly, showing local time, UTC time, and offsets And if Absolute UTC removes ambiguity, the resolution prompts are dismissed and Publish/Save is enabled And the selected anchoring mode is saved with the session and reflected in API/exports
Preview and Bulk-Resolve Affected Occurrences in Recurring Series
Given the host schedules a recurring series that spans a DST change When the system detects affected occurrences Then an Affected occurrences list displays each impacted date with before/after local times and offsets And the host can Apply resolution to all affected or Resolve individually per occurrence And a counter of unresolved items is shown and must be zero before Publish/Save is enabled
Block Publish with Clear Guidance Until All Issues Resolved
Given any ambiguous or invalid times remain unresolved When the host attempts to Publish/Save Then the action is blocked, the Publish/Save control is disabled, and an inline error explains the issue and links to the first unresolved occurrence And no draft or published session is created or updated And resolving all issues clears the error and enables Publish/Save
Audit Log Captures DST Resolution Decisions
Given a session or series with a DST ambiguity/gap is saved When the host completes a resolution (occurrence choice, adjustment, or anchoring change) Then an audit record is written capturing: user ID, timestamp, timezone, original local time, resolution type, resulting absolute time(s), impacted occurrence IDs/count, and client version And the audit record is retrievable via the admin audit log viewer and API And subsequent edits append new audit entries without mutating prior records

Quick Flip

One‑tap toggle between Local, Host, and Original time zones on schedules, rosters, and invoices—plus a handy “in X hours” relative view. Gives both staff and clients instant clarity without leaving the page, cutting confusion and support questions.

Requirements

Quick Flip Toggle UI
"As a client or instructor, I want to switch between Local, Host, Original, or Relative time views with one tap so that I can instantly understand class times without leaving the page."
Description

A unified, one-tap segmented control that switches between Local (viewer’s device time zone), Host (class venue/organizer time zone), Original (time zone used when the class was created), and Relative (“in X hours”) views. The control appears persistently on schedules, rosters, booking modals, and invoices without page reloads. It displays the active mode, shows the applied time zone abbreviation where relevant, and updates all visible timestamps instantly. Designed to be lightweight, responsive, and embeddable across pages, it minimizes visual clutter while providing immediate clarity. Includes graceful fallback for unsupported environments.

Acceptance Criteria
Instant Toggle on Schedule and Roster Pages
Given I am viewing a schedule or roster with at least 10 visible class timestamps in the default mode When I tap Local, Host, Original, or Relative on the segmented control Then all visible timestamps update to the selected mode within 150 ms without a full page reload or route change And the active segment reflects the selection visually and programmatically (aria-checked=true) And rapidly toggling between segments 10 times within 5 seconds results in no missed updates and no JavaScript errors
Booking Modal Time Toggle
Given I have opened a booking modal that displays a class start and end time When I select a different segment (Local, Host, Original, or Relative) Then all times shown in the modal update to the selected mode within 150 ms without closing or reloading the modal And no additional network requests are triggered by the toggle action And the selected segment remains persistent while interacting with other modal elements (e.g., capacity, payment)
Invoice Toggle and Time Zone Abbreviation
Given I am viewing an invoice with line items that include class date/time When I select Local, Host, or Original Then each visible timestamp is formatted in the selected time zone and displays the correct time zone abbreviation adjacent to the time (e.g., PST, CET) And when Relative is selected, timestamps display relative text and no time zone abbreviation And all visible timestamps update within 150 ms without a page reload
Time Zone Accuracy (Local, Host, Original) incl. DST and Mixed Hosts
Given a roster containing classes from multiple hosts in different time zones, including events on DST transition dates When Host mode is selected Then each class time is shown in its own host time zone using the correct offset for the event date (DST-aware) And when Local mode is selected, each time converts from the event instant to the viewer’s device time zone using IANA time zone data And when Original mode is selected, each time uses the class.creationTimeZone recorded at class creation And all conversions are accurate to the minute with no off-by-one-hour errors on DST change dates
Relative View Formatting and Auto-Update
Given an upcoming class scheduled 3 hours 40 minutes from now When Relative mode is selected Then the display shows "in 4 hours" (rounded to the nearest hour) And the relative text auto-updates at least once per minute without user interaction And for past classes, the display shows "X hours ago" using the same rounding rule And no time zone abbreviation is displayed in Relative mode
Responsive and Accessible Segmented Control
Given a viewport width of 320 px on a touch device When the segmented control renders Then each segment has a minimum tap target of 44 x 44 px and labels remain readable without horizontal scrolling And keyboard users can navigate with ArrowLeft/ArrowRight and activate with Enter/Space, with visible focus per WCAG 2.4.7 And the control implements an accessible radiogroup (role="radiogroup" on container, role="radio" on options) maintaining correct aria-checked state And toggling feedback (visual change and time updates) completes within 100 ms on a mid-tier device
Graceful Fallback in Unsupported Environments
Given JavaScript is disabled When a schedule, roster, booking modal, or invoice is loaded Then timestamps render in Original mode with a static "Original" label and the segmented control is not displayed Given the browser lacks Intl.DateTimeFormat timeZone support When the control is used Then conversions fall back to GMT±offset math with generic abbreviations (e.g., GMT-7) Given the viewer’s device time zone cannot be detected When the page loads Then the Local option is hidden or disabled with an explanatory tooltip, and Host becomes the default selection
Timezone Conversion Engine
"As a scheduler, I want times to convert accurately across time zones and DST so that I can avoid errors and trust the schedule."
Description

A centralized service that converts event times accurately using IANA time zones, accounting for daylight saving transitions, fractional offsets, and venue-based host time resolution. Provides a single source of truth for conversions on the client and server, exposes helpers for formatting (12/24‑hour), and computes continuously updating relative times (e.g., “starts in 3 hours”). Handles edge cases such as multi-day events, past events, and real-time clock drift, with unit tests and snapshot tests to guarantee correctness. Optimized with caching and memoization to ensure sub-50ms updates for typical views.

Acceptance Criteria
Cross‑Zone Conversion Accuracy with DST and Fractional Offsets
Given a set of ≥250 UTC instants spanning DST starts/ends and standard periods across ≥60 IANA zones (including fractional offsets like Asia/Kathmandu, Australia/LHI), When convert(instantUTC, zone, tzdbVersion) is executed, Then local time and UTC offset match authoritative tzdb results for 100% of cases with tolerance ≤1 second. Given an instant that falls within a DST fall-back overlap, When converting from UTC, Then the resolved offset corresponds exactly to the instant’s absolute position (no ambiguous local time outcome). Given tzdbVersion V in configuration, When conversions run, Then results align with V and an automated test suite verifies all vectors with 0 failures.
Quick Flip Timezone Toggles on Schedule, Roster, and Invoice Views
Given events with Original zone O, venue Host zone H, and viewer Local zone L, When the user toggles Quick Flip to Original/Host/Local, Then all visible timestamps in schedule, roster, and invoice sections re-render in O/H/L respectively and remain mutually consistent for start/end pairs. Given 200 visible events, When toggling zones, Then client-side re-render completes within 50 ms p95 without a full page reload, and server-rendered HTML (if used) matches client rendering byte-for-byte for the same inputs. Given a user session, When navigating between schedule, roster, and invoice on the same device, Then the last selected toggle persists for the session unless explicitly changed.
Continuously Updating Relative Time and Clock Drift Handling
Given relative time labels enabled, When an event start is in <1 hour, Then labels update at 1-second cadence; When 1–24 hours, Then labels update at 1-minute cadence; When past start, Then labels switch to “started X ago” and after end to “ended X ago” with the same cadence rules. Given a monotonic clock source and periodic server time beacons, When detected drift between system clock and server exceeds 2 seconds, Then the client resynchronizes and corrects all relative labels within 3 seconds without showing negative countdowns. Given 1,000 concurrent relative timers, When running for 10 minutes, Then CPU utilization for updates remains under 5% on a mid-tier device and no timer lags exceed 1 second p99.
Multi‑Day Event Integrity Across Time Zones
Given an event with start S and end E in Host zone H, When rendered in any IANA zone Z, Then the computed duration equals E−S exactly and is invariant across DST transitions and zone changes. Given events spanning midnight or multiple days, When formatted in any zone, Then start/end dates map to correct local calendar days and do not shift the event’s duration or swap dates. Given a test matrix of ≥100 multi-day events crossing at least 30 DST boundaries, When validated, Then 100% of cases preserve duration and correct local dates.
Formatting Helpers: 12/24‑Hour and Locale‑Aware Outputs
Given format=12h, When formatting a local time, Then the output uses h:mm a (e.g., 1:05 PM) with no leading zero on hour and lowercase/uppercase per locale; Given format=24h, Then the output uses HH:mm (e.g., 13:05) with leading zero. Given a locale tag (e.g., en-US, en-GB, fr-FR), When formatting full date-times, Then day/month order, separators, and month/day names follow the locale, and numerals are localized where applicable; When locale is absent, Then default is en-US. Given snapshot tests over 50 representative instants across 20 zones and 6 locales, When executed, Then 100% of snapshots match and any tzdb version change produces only intentional, reviewed diffs.
Caching and Memoization Performance for Typical Views
Given 200 events visible and three zone modes (Local/Host/Original), When first rendering, Then conversion+formatting completes in ≤200 ms p95 on a standard mid-tier device; When subsequent toggles or periodic relative-time ticks occur, Then updates complete in ≤50 ms p95. Given identical (instant, zone, format, locale) inputs, When converting repeatedly, Then cached results are reused with a hit rate ≥90% after warm-up; When tzdb version, zone, format, or locale changes, Then affected cache entries are invalidated within 1 second. Given sustained navigation, When cache exceeds 3,000 distinct keys, Then least-recently-used entries are evicted and memory overhead remains ≤5 MB per additional 1,000 cached keys.
Server–Client Consistency and Test Coverage
Given the same input payload (instant, zone, format, locale, tzdbVersion), When processed by server helper and client helper, Then produced ISO timestamps and formatted strings are bitwise identical for 10,000 random instants across 100 zones with 0 mismatches. Given the timezone engine codebase, When running CI, Then unit test coverage is ≥95% lines and ≥90% branches, property-based tests for conversions run ≥10,000 cases with 0 failures, and snapshot tests for formatting across 20 locales/zones pass 100%. Given API v1 of the conversion helpers, When breaking changes are introduced, Then a new version is published and older versions remain unchanged and fully passing until formally deprecated.
Cross-Page TZ Context & API
"As a user and integrator, I want a consistent time-zone context across the UI and API so that shared links and data align and prevent booking mistakes."
Description

A shared time-zone context that propagates the selected view across schedules, rosters, invoices, and booking flows. Deep-link support via query parameters (e.g., tz_mode and tz) ensures consistent display when sharing URLs. Back-end and public API endpoints accept and return canonical UTC plus selected time-zone metadata, preventing discrepancies between UI and integrations. State persists during navigation without reloads, and batch updates re-render timestamps efficiently to prevent layout thrashing.

Acceptance Criteria
Persisted TZ Context Across Navigation
Given the user selects tz_mode=host via Quick Flip on the Schedule page, When they navigate to Rosters and Invoices without a full page reload, Then all timestamps render in Host time with no flicker and the Quick Flip shows Host selected. Given the user selects tz_mode=explicit&tz=America/Chicago, When they enter the Booking Flow modal and return to the Schedule, Then the explicit tz remains active across both surfaces. Given the user uses the browser Back/Forward buttons between pages that were visited with different tz modes, When the history state is applied, Then the previously active tz mode for that history entry is restored. Given the user toggles between modes, When viewing relative "in X hours" badges, Then the relative values remain numerically consistent across modes for the same events.
Deep-Link TZ Parameters Applied on Page Load
Given a URL with ?tz_mode=explicit&tz=Europe/Berlin, When the page first renders, Then all timestamps display in Europe/Berlin on first paint and the Quick Flip indicates Explicit (Europe/Berlin). Given a URL with only ?tz_mode=local, When the page loads, Then the user's device time zone is applied and no tz parameter is required. Given a URL without tz parameters, When the page loads, Then the default mode is host unless a prior selection exists in session storage, in which case the prior selection is applied. Given a URL with tz_mode and tz provided, When the user toggles modes, Then the URL query is updated to reflect the current selection without causing a full reload.
API UTC Canonical with TZ Metadata Round-Trip
Given a GET request to /api/v1/classes with tz_mode=explicit&tz=America/Los_Angeles, When the response is returned, Then each item includes canonical UTC start/end fields and tz metadata fields tzMode, tz, and offsetMinutes consistent with America/Los_Angeles at the event instant. Given a POST to create a booking including startTimeUtc and tz metadata, When the server validates the payload, Then if the provided local time derived from tz metadata does not match startTimeUtc, the server responds 400 with a validation error. Given the UI renders a list fetched with tz parameters, When comparing displayed local times with server-provided conversions, Then differences are <= 1 minute across 50 sampled events spanning different time zones and DST boundaries. Given no tz parameters are supplied, When hitting the endpoint, Then the response uses tzMode=host in metadata.
Batch Re-render Efficiency on TZ Change
Given a schedule view with 200 visible timestamps, When the user switches tz_mode via Quick Flip, Then all visible timestamps update within 150ms on a mid-tier device and maintain >= 55 FPS during the transition. Given the same scenario, When performance is profiled, Then no more than 2 layout reflows and no more than 1 style recalculation per container are recorded. Given the user is scrolled mid-list, When toggling tz mode, Then the scroll position does not shift by more than 2px and no content jumps occur. Given the app re-renders timestamps, When inspecting the DOM, Then text nodes are updated in a single batched frame per view.
Robust TZ Param Validation and Fallback
Given tz_mode is not one of [local, host, original, explicit], When the page loads, Then tz_mode falls back to host and an internal warning is logged. Given tz_mode=explicit with tz not a valid IANA time zone, When applied, Then the app falls back to host and removes the invalid tz from the URL. Given tz is provided with tz_mode in [local, host, original], When applied, Then tz is ignored and removed from the URL to avoid confusion. Given tz_mode and tz are conflicting across multiple sources (URL vs in-memory), When resolving, Then URL parameters take precedence on initial load; thereafter the in-memory context governs until changed.
TZ Context Reflected in and Propagated via URLs
Given a user has an active tz selection, When clicking internal links to schedules, rosters, invoices, or booking flows, Then the destination URLs include the same tz_mode and tz parameters. Given the user copies the current URL and shares it, When a recipient opens it, Then they see identical times and tz selection as the sharer. Given the user changes tz selection, When the URL is updated, Then only a single tz_mode and at most one tz parameter are present and duplicates are not introduced. Given the user clears the tz selection via a reset control, When applied, Then tz parameters are removed from the URL and default behavior (host or prior session selection) resumes.
DST and Ambiguous Local Times Correctness
Given events that occur within 24 hours around DST transitions across at least 10 IANA zones, When displayed in explicit and local modes, Then offsets and wall times match the IANA database with no off-by-one-hour errors. Given events scheduled at an ambiguous local time during fall-back, When displayed, Then the correct occurrence is selected based on the event's UTC instant and, if formatting supports it, the offset is shown to disambiguate. Given a list sorted by startTimeUtc, When toggling between tz modes, Then the on-screen order of items does not change. Given relative "in X hours" badges, When toggling tz modes, Then the badge values remain consistent for the same events because they are computed from UTC.
Preference Persistence & Defaults
"As a returning user, I want the app to remember my preferred time view so that I don’t have to reconfigure it on every visit."
Description

Per-user and per-organization defaults for the selected time view. Logged-in users’ preferences are stored in profile settings; guests use secure local storage with sensible TTL. Admins can set workspace-wide defaults and optional class-level overrides (e.g., always show Host for in-person). URL parameters can temporarily override defaults for campaigns and embeds. Includes a clear reset mechanism and respects privacy/consent requirements.

Acceptance Criteria
Logged-in Preference Persists Across Sessions and Surfaces
Given a logged-in user selects a time view (Local, Host, Original, or Relative) using Quick Flip on any page (schedule, roster, invoice) When they navigate to another eligible page or sign in from another device Then the selected time view is applied consistently across schedules, rosters, and invoices without additional action And the preference is stored in the user's profile within 500ms of the change (p95) And the UI reflects the applied view within 200ms without a full page reload And if saving the preference fails, the UI informs the user and retries up to 3 times with exponential backoff, then falls back to the last known good preference
Workspace Default Applies When No Personal Preference
Given an admin sets the workspace-wide default time view to Local, Host, Original, or Relative When a member without a personal preference loads any schedule, roster, or invoice Then the workspace default is applied for that member And changing the selection creates a personal preference that supersedes the workspace default for that member thereafter And existing members with a personal preference remain unaffected by the workspace default change And the admin action is recorded in audit logs with actor, timestamp, scope, previous value, and new value
Class-Level Override Behavior (Default vs Enforced)
Given a class has a time view override set to Default:Host When any user views that class context Then Host is the initial selection for that class, but users may switch views for their session/preference as allowed And switching does not modify the class override Given a class has a time view override set to Enforce:Host When any user views that class context Then the time view is locked to Host and other options are disabled with an explanatory tooltip And URL parameters do not bypass an Enforced override
Guest Preference Stored with TTL and Consent
Given a guest user toggles the time view and has granted consent for preference storage When the selection is made Then only the selected view and an expiry timestamp (TTL = 30 days) are written to same-origin storage And on subsequent visits within TTL the stored view is applied automatically And after TTL expiry the view resets to defaults and the expired record is cleared on next read Given a guest user has not granted consent When they toggle the time view Then the selection is stored only for the current session and cleared on tab or browser close And no persistent storage is written Given a guest clicks Reset Time View When they confirm Then all stored guest preferences are deleted immediately and defaults are applied
URL Parameter Temporary Override for Campaigns and Embeds
Given a page or widget is loaded with tz parameter (tz=local|host|original|relative) When the page renders Then the specified view is applied for that context And an indicator shows the override is active And the override does not update a logged-in user's saved preference unless the user explicitly chooses “Make default” And the override persists only where the parameter is present (e.g., current page or embed) and does not affect other pages without the parameter And invalid tz values are ignored with safe fallback to the normal precedence order Given an embedded widget is loaded with a valid tz parameter When users interact within the embed Then the override applies only inside the embed and does not change site-wide preferences
Reset Mechanism Clears and Reapplies Defaults
Given a user (guest or logged-in) clicks Reset Time View When they confirm the action Then all stored time view preferences for that user context are cleared (profile setting for logged-in; local/session storage for guest) And the current page immediately re-renders using the effective default based on precedence And a success message confirms the reset And for logged-in users, subsequent sessions and devices reflect the reset on next load
Precedence, Privacy, and Auditability Rules
Rule: Precedence for the applied time view is, from highest to lowest: URL parameter (not applicable when class override is Enforced) > Enforced class-level override > user personal preference > class-level default > workspace default > system default (Local) Rule: Only minimal, non-PII data (selected view and timestamps) is stored for preferences; all client-side storage keys are namespaced to ClassTap and environment Rule: Users can view and delete their personal time view preference in profile settings; Admins can view but cannot change a user's personal preference Rule: All admin changes to workspace defaults and class-level overrides are logged with actor, timestamp, scope, and before/after values Rule: Preference save/read operations meet performance SLOs (p95 <= 500ms, success rate >= 99.9% over rolling 24h); on failure, the system degrades to the next item in the precedence chain without breaking page rendering
Accessibility & Localization for Quick Flip
"As an accessibility-minded user, I want the time toggle to be keyboard and screen-reader friendly and localized so that I can use it efficiently in my language and on my device."
Description

WCAG 2.2 AA-compliant control with full keyboard navigation, focus indicators, ARIA roles/states, and screen-reader-friendly labels (including expanded time-zone names). Supports high-contrast themes, touch targets ≥44px, and robust error states. Localizes labels and time-zone abbreviations, honors user 12/24-hour preferences, and formats times per locale conventions. Includes automated a11y tests and pseudo-localization in CI.

Acceptance Criteria
Keyboard, Focus, and High-Contrast for Quick Flip
Given Quick Flip is present on a page, When the user presses Tab from the preceding element, Then the first Quick Flip option receives focus in logical reading order. Given a Quick Flip option has focus, When the user presses ArrowRight or ArrowLeft, Then focus moves within the group and the selected option updates without moving the Tab focus out of the group. Given a Quick Flip option has focus, When the user presses Space or Enter, Then that option becomes selected and visible times update without a full page reload. Given Quick Flip is focused, When the user presses Tab or Shift+Tab, Then focus exits to the next or previous focusable element with no keyboard trap. Given any Quick Flip focusable element receives focus, Then a visible focus indicator is present with at least 3:1 contrast against adjacent colors and is not obscured by sticky UI. Given the OS is in High Contrast/Forced Colors mode, When navigating Quick Flip, Then text and focus indicators remain visible and meet WCAG 2.2 AA contrast requirements using system colors.
Screen Reader Semantics and Expanded Time-Zone Names
Given a screen reader user focuses Quick Flip, Then the control announces an accessible name that includes "Time zone" and the current selection. Given a screen reader user navigates Quick Flip options, Then options are exposed as a single-select group with correct roles and states (e.g., radiogroup/radio or segmented toggle) and the selected state is programmatically conveyed. Given a time-zone option is shown with an abbreviation in the UI, Then its accessible name announces the expanded time-zone name along with the abbreviation. Given the selected time zone changes, When schedule times on the page update, Then the change is conveyed via a polite live region without re-announcing the entire control.
Touch Target Size and Pointer-Tap Support
Given a touch device, When a user taps any Quick Flip option, Then the tap activates successfully with a hit area of at least 44x44 CSS pixels. Given adjacent Quick Flip options, When tested, Then hit targets do not overlap and inadvertent activation rate is minimized by adequate spacing or extended hit areas. Given a user interacts with Quick Flip via pointer, When activating an option, Then a single tap or click is sufficient and no complex gestures are required.
Localization and 12/24‑Hour Time Formatting
Given a user preference of 12-hour time, When Quick Flip renders times, Then times display in localized 12-hour format with localized AM/PM markers. Given a user preference of 24-hour time, When Quick Flip renders times, Then times display in 24-hour format without AM/PM markers. Given the app locale changes, When Quick Flip renders labels and time zones, Then all labels and time-zone names/abbreviations are localized and date/time punctuation and ordering follow the locale conventions. Given an RTL locale is active, When Quick Flip renders, Then layout mirrors correctly and strings render with correct directionality.
Relative 'In X Hours' View Accessibility and Localization
Given the user enables the relative view, When an upcoming class is 3 hours away, Then the UI displays a localized phrase equivalent to "in 3 hours" with correct pluralization for the locale. Given time passes and a threshold is crossed (e.g., from 2 hours to 1 hour), When one minute elapses, Then the relative phrase updates accurately without a page reload and changes are announced via a polite live region for assistive technologies. Given a screen reader user focuses the relative phrase, Then the accessible description includes both the relative phrase and the absolute date/time formatted per the user’s preferences.
Error Handling and Accessible Announcements
Given time-zone data fails to load, When Quick Flip initializes, Then an inline, localized error message is shown with role="alert" and a retry action is available. Given a network error occurs during a selection change, When the user selects a new time zone, Then the control shows a non-blocking, localized error toast and reverts to the last valid selection with no loss of keyboard focus. Given an unsupported or invalid user time-format preference, When rendering, Then Quick Flip falls back to a default locale-appropriate format and logs a non-fatal telemetry event.
Automated Accessibility and Pseudo-Localization in CI
Given the CI pipeline runs, When automated accessibility tests execute on Quick Flip views, Then there are zero axe-core violations at critical and serious impact levels. Given pseudo-localization is enabled in CI, When Quick Flip renders, Then all user-facing strings appear pseudo-localized with at least 30% text expansion and no truncation or overflow is detected in visual regression tests. Given a locale test matrix (at least en, es, de, fr, ja, ar), When unit and integration tests run, Then assertions for date/time formatting, 12/24-hour preference, and localized time-zone names pass across all locales. Given cross-browser E2E tests on Chrome, Firefox, and Safari (latest two versions), When keyboard navigation scenarios execute, Then all steps pass without flakiness beyond the defined threshold.
Export & Notification TZ Consistency
"As a client receiving emails or calendar invites, I want the shown times to match my selected view and clearly indicate the time zone so that I don’t miss classes due to ambiguity."
Description

Ensures exports and outbound communications reflect the selected time view while remaining unambiguous. ICS files include TZID and embedded VTIMEZONE; PDFs, invoices, and printed views display the chosen mode and a clear time-zone label; emails/SMS show both the selected view and an absolute time with time-zone to avoid confusion when forwarded. All templates pull from the same conversion engine and include a header note (e.g., “Times shown in America/Los_Angeles”).

Acceptance Criteria
ICS Calendar Export Carries TZID and VTIMEZONE
Given a user has selected a time view (Local, Host, or Original) When the user exports an ICS file for any event or schedule Then each VEVENT contains DTSTART and DTEND with a TZID matching the selected view's IANA time zone And the ICS contains a corresponding VTIMEZONE component for every TZID used And the converted local times match the same UTC instants produced by the conversion engine for the selected view And recurring events transition across DST with correct local times and offsets in the ICS And if the relative view (in X hours) is active, the ICS still renders absolute times using the selected underlying time zone (no relative-only times)
PDF/Invoice/Print Show Selected Mode and Clear TZ Label
Given a user has selected a time view (Local, Host, or Original) When a PDF, invoice, or printed view is generated Then the document header includes the selected mode label and the note "Times shown in {IANA_TZ}" And all displayed times in the document are rendered in that time zone And if Original mode includes events from multiple time zones, each line item shows its event-specific IANA time zone and GMT offset alongside the time And the document contains no unlabeled times
Emails/SMS Include Selected View Plus Absolute Time with TZ
Given a user has selected a time view (Local, Host, Original, or relative in X hours) When an email or SMS (confirmation, reminder, waitlist-to-payment, receipt) is sent Then each event time is shown in the selected view with its mode label and IANA time zone And each event time also includes an absolute representation with explicit time zone and offset (e.g., 2025-03-10 17:00 UTC or UTC−07:00) And for the relative view, the message includes the relative string (e.g., "in 3 hours") plus the absolute time with time zone And the message contains no time without an explicit time-zone context
CSV/Roster Exports Reflect Selected View with TZ Columns
Given a user has selected a time view (Local, Host, or Original) When exporting a CSV roster, schedule, or invoice data Then all event time columns are rendered in the selected time view And the export includes a Time Mode column with values Local/Host/Original And the export includes a Time Zone column using IANA names; in Original mode, the column reflects the per-row event time zone And no time values appear without an associated time-zone context
Unified Conversion Engine Consistency Across Channels
Given a single conversion engine is configured for all outputs When the same event is rendered in the same selected view across ICS, PDF, print, email, SMS, and CSV Then the displayed local times represent the same UTC instants across all channels And switching the selected view (Local ↔ Host ↔ Original) produces consistent, corresponding changes across all channels And no channel deviates by more than 0 minutes from the engine's computed local time for the same event and view
DST and Ambiguous/Non-Existent Local Time Handling
Given events scheduled near DST transitions in any supported time zone When times are exported or communicated in any channel Then ambiguous times (e.g., during fall-back) display with IANA time zone and explicit GMT offset to disambiguate And non-existent times (e.g., spring-forward gaps) are normalized per the conversion engine and displayed with the resulting offset and IANA zone And ICS outputs rely on TZID+VTIMEZONE to unambiguously represent the occurrence And human-readable outputs contain no time lacking either a zone label or offset

Global Time Finder

Suggests high‑performing class times across selected regions using attendee time‑zone distribution, past attendance, and quiet‑hour rules. Visual heat bands highlight “best” windows so you can pick slots that maximize conversions and minimize no‑shows.

Requirements

Time-Zone Distribution Aggregation
"As an instructor, I want ClassTap to aggregate when my audience is available across time zones so that I can schedule classes when most people can attend."
Description

Aggregate attendee availability across selected regions by combining user profile time zones, historical attendance timestamps, lead capture locations, and waitlist data to model local-time distributions. Normalize for daylight saving time changes, anonymize data, and provide fallbacks when historical data is sparse. Expose an aggregated distribution service that refreshes nightly and on-demand, feeding downstream scoring and visualization while maintaining low latency and data privacy safeguards.

Acceptance Criteria
Nightly Aggregation Refresh Across Regions
Given the scheduled 02:00 UTC daily window and a tenant with Global Time Finder enabled and selected regions When the nightly aggregation job runs Then aggregated local-time distributions per tenant-region are recomputed using user profile time zones, historical attendance timestamps, lead capture locations, and waitlist data from the last 180 days And all event timestamps are converted to attendees’ local time with DST normalization And the new version is written atomically with version_id and last_refresh_time And downstream scoring/visualization read the new version within 5 minutes of job completion And the job completes within 60 minutes p95 across all tenants And partial failures are retried up to 3 times and surfaced via metrics and alerting And no active distribution older than 24 hours remains after the window closes
On-Demand Aggregation Trigger by Admin
Given an authenticated tenant admin initiates an on-demand refresh via UI or API and the tenant has not exceeded 1 refresh per 10 minutes When the request is submitted with selected regions Then aggregation runs only for that tenant and specified regions And the API responds immediately with 202 Accepted and a refresh_request_id And the refresh completes within 120 seconds p95 And the resulting version is swapped in without downtime and the ETag changes And the UI shows the new last_refresh_time within 1 minute of completion And requests beyond the rate limit return 429 with a retry_after value
DST Normalization Across Regions
Given historical attendance that spans DST transitions for America/New_York and Europe/London and non-DST region Asia/Tokyo When distributions are computed Then events are bucketed by local wall-clock time using correct IANA tz rules effective at each event timestamp And the duplicated hour during fall back is represented as distinct wall-clock buckets without double-counting totals And the missing hour during spring forward does not introduce artificial zeros by proportionally redistributing same-day counts And resulting per-half-hour bins differ by no more than 2% from expected control fixtures in unit tests
Data Privacy and Anonymization Thresholds
Given aggregated distributions are per tenant-region and binned at 30-minute intervals When a bin contains fewer than 10 unique contributing attendees across all sources Then the bin is suppressed or merged with adjacent bins until k >= 10 And no raw identifiers (emails, IPs, user IDs) or per-user timestamps are stored in or returned by the aggregation store or API And event-level records are processed in-memory only during aggregation and not persisted post-run And API responses include privacy_applied=true and k_min in metadata And any request for per-user or unbinned data returns 400
Sparse Data Fallback Logic
Given a tenant-region has <100 historical attendance events in the last 180 days or <20 unique attendees When computing the distribution Then the model combines sources with weights: historical attendance 40%, lead capture locations 30%, waitlist requested times 30% And if thresholds remain unmet, user profile time zones contribute a uniform prior over 08:00–20:00 local at 20% weight with all weights renormalized to 100% And the response includes confidence_score (0.0–1.0) and fallback_reason And if no sources are available, a regional baseline is returned with confidence_score <= 0.3 And all applied fallbacks are listed in response metadata
Low-Latency Aggregated Distribution API
Given an authenticated request with tenant_id and a list of region identifiers When GET /v1/aggregation/distribution is called with ETag/If-None-Match headers Then cache hits return 304 within 100 ms p95 And cache misses return 200 with JSON including version_id, last_refresh_time (UTC), bins (48 half-hour bins per region with local-time labels), data_sources_used[], and confidence_score within 300 ms p95 and 800 ms p99 And payload size is <=128 KB per region and <=1 MB per request And invalid/unauthorized requests return 400/401/403 accordingly; unknown regions return 404 And the service sustains >=200 RPS while meeting latency targets
Attendance-Weighted Scoring Engine
"As a studio owner, I want a data-driven score for each potential time slot so that I can choose times that maximize attendance and revenue while lowering no-shows."
Description

Compute a time-slot score (0–100) for a configurable grid that blends attendee time-zone distribution, past attendance by class type, conversion rates, no-show probability, quiet-hour rules, and region-specific seasonality. Support class duration and modality (online/in-person), provide tunable weighting presets (e.g., maximize revenue vs. minimize no-shows), and return reason codes/feature attributions to explain recommendations. Deliver results via an internal API with <500ms response for weekly views and cache hot queries for responsiveness.

Acceptance Criteria
Composite Scoring Across All Factors
Given a configured grid for a specific class type, selected regions, and all factors enabled When the engine computes scores for a 7-day view Then each slot score uses attendee time-zone distribution, past attendance for the class type, conversion rates, no-show probability, quiet-hour rules, and region-specific seasonality And scores are integers from 0 to 100 inclusive And the same inputs and weights always produce identical scores And slots in a region’s quiet hours receive a penalty proportional to the configured quiet-hour weight
Duration and Modality Support
Given class durations of 45, 60, and 90 minutes and both online and in-person modalities When the engine scores a weekly grid Then the score for a duration spans and aggregates all constituent time bins covered by the class And modality-specific baselines for conversion and no-show are applied And scores change appropriately when modality changes with differing baselines, all else equal
Weighting Presets and Custom Tunability
Given presets Maximize Revenue and Minimize No-Shows and a valid custom weighting When scoring the same weekly grid Then presets are selectable via API and are echoed in response metadata And the top-5 ranked slots differ between the two presets when trade-offs exist in the data And custom weighting overrides presets and is validated to sum to 100% with each factor weight between 0 and 100% And invalid weights return HTTP 400 with a descriptive error code
Reason Codes and Factor Attributions
Given a scored weekly grid requested via API When results are returned Then each slot includes at least three reason codes with signed factor contributions And the sum of factor contributions equals the slot score within ±1 due to rounding And reason codes include machine-readable keys and human-readable messages And contributions are sorted by absolute impact descending And include=all_reasons returns the full list of factor contributions
Multi-Region Aggregation and Quiet-Hour Rules
Given selected regions with distinct time zones, quiet hours, and seasonality profiles When the engine scores a weekly grid Then attendee distributions are converted to local time per region before aggregation And per-region quiet hours are applied prior to aggregation And final slot scores are computed using attendance-weighted aggregation across regions
API Performance and Caching for Weekly Views
Given a weekly view request with identical parameters repeated within the cache TTL When the first request is cold and subsequent requests are repeated Then the 95th percentile latency for subsequent identical requests is ≤ 500 ms measured at the API boundary And response metadata includes cache=HIT for cached responses and cache=MISS for cold responses And the cache key includes tenant, regions, class type, duration, modality, date range, and weighting preset or custom hash
Score Range, Validity, and Monotonicity
Given any valid input and weighting configuration When the engine computes scores Then all slot scores are finite numbers in the range 0–100 inclusive And increasing the weight of a positively correlated factor (e.g., past attendance) does not decrease a slot’s score, all else equal And increasing the weight of an inversely correlated factor (e.g., no-show probability) does not increase a slot’s score, all else equal And ties are deterministically broken by a stable secondary key
Heat Band Calendar Visualization
"As an instructor, I want to see a heatmap of best times across regions so that I can quickly spot high-performing windows without crunching numbers."
Description

Render a responsive calendar with visual heat bands that map score quantiles across days and times. Provide tooltips with expected attendance range, conversion uplift, and no-show risk, plus filters for region, class type, duration, and modality. Ensure accessibility with colorblind-safe palettes, keyboard navigation, and clear legends. Enable quick interactions via client-side caching, and support export to PNG/CSV and shareable links with permission controls.

Acceptance Criteria
Responsive Heat Bands Across Viewports
Given the calendar loads with default filters, When the viewport width changes between 320px and 1440px, Then the heat band grid reflows without overlap or truncation and all time labels remain readable. Given score quantiles Q1 to Q5 are provided, When the calendar renders, Then each time cell displays a color corresponding to its quantile per the legend, and the highest quantile cells are visually emphasized with a best indicator. Given a week or month navigation action, When the user moves to the next or previous range, Then the calendar renders the new range within 500 ms on cached data and within 2 s on first load.
Insight Tooltips for Time Cells
Given a user hovers or keyboard-focuses a time cell, When the tooltip is triggered, Then it appears within 150 ms and contains expected attendance min-max, conversion uplift percent versus baseline, and no-show risk percent. Given the tooltip may exceed viewport bounds, When it would overflow, Then it repositions to remain fully visible. Given screen reader users, When the time cell receives focus, Then the tooltip content is exposed via aria-describedby with the same values. Given a selected region or timezone, When the tooltip displays times, Then times are shown in the selected timezone.
Filter Controls Update Calendar
Given filters for region (multi-select), class type (multi-select), duration (15-180 minutes), and modality (in-person, virtual, hybrid), When the user applies any combination, Then the calendar updates to reflect the filtered dataset within 500 ms on cached data and within 2 s on first load. Given an invalid filter combination yields no results, When applied, Then the calendar shows a clear empty state message and the legend remains visible. Given a filter reset action, When reset is triggered, Then all filters return to defaults and the calendar and legend update accordingly.
Accessibility and Colorblind-Safe Visualization
Given WCAG 2.1 AA requirements, When the heat bands and legend render, Then all color and text combinations meet contrast ratio >= 4.5:1 and use a colorblind-safe palette validated for deuteranopia, protanopia, and tritanopia. Given keyboard-only navigation, When tabbing into the calendar, Then users can move between time cells using arrow keys with a visible focus indicator and no keyboard trap. Given screen reader users, When the calendar renders, Then each cell announces date, time range, quantile label, and best if applicable. Given the legend is present, When rendered, Then it labels quantile-to-color mapping and includes a text alternative readable by assistive tech.
Client-Side Caching for Fast Interactions
Given a previously viewed calendar range with identical filters, When navigating back or forward between ranges, Then data is served from client cache without a network request and renders within 300 ms. Given filter or date range parameters change from the cached key, When navigating or applying filters, Then a fresh fetch occurs and the cache is updated with the new response. Given cache staleness protection, When cached data is older than 15 minutes or the server signals a new data version, Then the client invalidates the cache and fetches fresh data.
Export to PNG and CSV
Given the current calendar view and filters, When the user selects Export PNG, Then a PNG downloads within 3 s containing the visible date range, heat bands, legend, title, generation timestamp, and timezone label. Given the current dataset, When the user selects Export CSV, Then a CSV downloads within 3 s containing one row per time slot with columns: date, start_time, end_time, score, quantile, expected_attendance_min, expected_attendance_max, conversion_uplift_percent, no_show_risk_percent, region, class_type, duration_minutes, modality. Given no data is available, When an export is attempted, Then the export controls are disabled or a message indicates no data to export.
Shareable Links with Permission Controls
Given a signed-in organizer, When generating a shareable link, Then a URL is created that encodes the current filters and date range and is accessible only to users with View Insights permission. Given an unauthorized user, When accessing the shareable link, Then the system returns 403 and prompts sign-in where applicable. Given the organizer revokes the shared link, When a previously issued link is accessed, Then access is blocked within 60 seconds and returns 410 Gone. Given an authorized viewer opens the link, When the page loads, Then the calendar renders the shared read-only view with non-destructive interactions only unless elevated permissions are granted.
Quiet Hours & Blackout Rules Management
"As a studio manager, I want to define quiet hours and blackout dates so that suggestions avoid times that hurt conversions or violate constraints."
Description

Offer a rule builder to define quiet hours by region/time zone and day of week, plus blackout dates for holidays and personal constraints. Provide market defaults from public calendars with the ability to override per class or campaign. Validate rules against existing schedules, propagate them to the scoring engine and UI masking, and maintain version history and audit logs. Support import from external calendars and respect user locale settings.

Acceptance Criteria
Create Quiet Hours by Region and Day
Given I have admin access and have selected Region = "US/Pacific" When I create a quiet-hours rule for Mon–Fri between 22:00–06:00 local time Then the rule is saved with timezone "America/Los_Angeles" and applies to Mon–Fri And the UI displays the quiet window in local time per my locale settings And overlapping quiet-hours for the same region are prevented with a clear validation error And the API responds 201 Created with ruleId and normalized UTC intervals
Define Blackout Dates for Holidays and Personal Constraints
Given market holiday defaults are enabled for Region = "UK" When I add blackout dates for 2025-12-25 (all day) and 2025-10-10 09:00–12:00 local as a personal constraint Then the calendar prevents creating or moving classes into those intervals And partial-day and recurring blackouts (e.g., first Monday monthly 08:00–10:00) are supported and displayed And precedence is enforced: Blackout > Quiet Hours for scheduling blocks And the API responds 201 Created and returns blackoutIds with UTC-normalized ranges
Validate Rules Against Existing Schedules
Given there are scheduled classes in US/Eastern on Tue 23:30–00:30 When I save a new quiet-hours rule for Tue 22:00–06:00 in US/Eastern Then the system detects conflicts and lists the affected classes with counts and IDs And I must choose to reschedule, cancel, or acknowledge per role policy before saving And the final save is blocked until all conflicts are resolved or explicitly acknowledged And an audit entry records the decision, userId, timestamp, and impacted classIds
Market Defaults and Overrides per Class/Campaign
Given public holiday calendars are enabled for Country = "DE" When I create a class and set an override to ignore a default holiday blackout Then that class can be scheduled on the holiday while the campaign defaults remain intact And campaign-level overrides apply to all classes in the campaign unless a class-level override exists And precedence is applied: Class Override > Campaign Override > Market Default And the UI labels overridden rules and the API includes overrideSource in responses
Rule Propagation to Scoring Engine and UI Masking
Given the Global Time Finder generates scores and heatmap bands When quiet hours exist for Asia/Singapore on weekdays 01:00–05:00 local and a full blackout on 2025-12-25 Then candidate slots in blackout windows are unselectable and hidden in the picker And candidate slots in quiet hours are selectable but dimmed, with score reduced by at least 50% versus comparable non-quiet slots And the scoring API excludes blacked-out intervals and returns justification codes for quiet-hour penalties And the heatmap legend shows distinct styles for blackout vs quiet hours
Version History and Audit Trail with Revert
Given versioning is enabled for rule changes When I create, edit, or delete a quiet-hour or blackout rule Then a new immutable version is recorded with versionId, userId, ISO timestamp, and change diff And I can revert to a prior version and the system reapplies the historical rules without data loss And exporting the audit log yields a CSV with userId, action, IP address, and affected ruleIds And only Admins can revert; Editors can create/edit; Viewers have read-only access
External Calendar Import and Deduplication
Given I connect Google Calendar and upload an ICS file for the same account When I import events tagged "Out of office" or "Holiday" Then blackout rules are created with correct local start/end using each event’s timezone And duplicates across sources are deduplicated by iCal UID and content hash And malformed events are skipped with error details (event UID or line number) shown in the import report And subsequent syncs complete within 15 minutes and only process deltas since the last sync
Multi-Region Selection & Weighting
"As an instructor, I want to choose and weight the regions I serve so that recommendations reflect where my demand actually is."
Description

Allow users to select multiple regions/cities and assign weights reflecting audience size or strategic priorities. Auto-suggest regions from customer data, deduplicate overlapping audiences, and handle DST and region-specific holidays. For in-person classes, apply a configurable travel radius to refine demand. Persist selections by class type and pass normalized weights to the scoring pipeline.

Acceptance Criteria
Multi-Region Selection & Weight Entry
Given I open the Global Time Finder region selector for a class When I search and add multiple regions or cities Then each selected region displays an editable Weight field and a remove control Given at least one region is selected When I enter weights Then the field accepts numeric values >= 0 with up to two decimal places and rejects negative/non‑numeric input with an inline error per field Given any weight field has a validation error or the sum of all weights equals 0 When I attempt to Save Then the Save action is blocked and an error message explains the issue Given all weights are valid and the total weight > 0 When I Save Then the selection and raw weights are stored and a success confirmation is shown
Auto‑Suggest Regions From Customer Data
Given the account has attendee location data When I open the region selector Then an Auto‑suggest list shows up to 10 regions ranked by attendee count, each showing region name and attendee count/percentage Given suggestions are displayed When I click Add on a suggestion Then the region is added to my selection once without creating duplicates Given the account has insufficient location data When I open the region selector Then the Auto‑suggest section shows an informative empty state and manual search remains available
Overlapping Audience Deduplication
Given I have selected overlapping geographies (e.g., a city and its encompassing state) When I Save the selection Then the system deduplicates overlapping audiences so that no attendee is counted more than once in downstream scoring Given deduplication occurs When I view the selection summary Then a dedup summary indicates the number of overlapping attendees removed and the effective audience per region
DST and Regional Holiday Handling
Given I have selected regions in different time zones and set a date range that includes a DST transition for at least one region When I generate best‑time suggestions Then the system uses each region’s local time (IANA time zones) so suggested windows align with local clocks across DST boundaries Given any selected region has a region‑specific holiday within the date range When I generate best‑time suggestions Then quiet‑hour rules are applied to that region only and not to others
In‑Person Travel Radius Filter
Given the class type is In‑person and a class location is set When I open Global Time Finder Then a Travel radius control is visible with units per account settings and a default value Given I adjust the travel radius When I view attendee demand and suggestions Then attendees outside the radius are excluded from demand estimates and suggestions update accordingly Given the class type is Online or no class location is set When I open Global Time Finder Then the Travel radius control is hidden (Online) or disabled with guidance to set a location (no location)
Persistence by Class Type
Given I have saved a region selection and weights for Class Type A When I reopen Global Time Finder for Class Type A Then the previously saved regions and weights are restored Given Class Type B has a different saved selection When I switch to Class Type B Then Class Type B’s regions and weights are shown, not Class Type A’s Given I clear all selections and Save for a class type When I reopen Global Time Finder for that class type Then no regions are selected
Normalized Weights Passed to Scoring Pipeline
Given I have selected regions with raw weights When I click Find Times Then the scoring request payload includes, per region, regionId, timezone, holidayCalendarId, dedupKey (if used), rawWeight, normalizedWeight, and radiusMeters (for in‑person) Given the scoring request is built When I inspect the payload via the developer console or logs Then normalizedWeight values sum to 1.0 ± 0.0001 after excluding zero‑weight regions and applying deduplication Given all raw weights are zero When I click Find Times Then the system prevents the request and displays an error indicating weights must sum to a positive value
One-Click Schedule & Conflict Guard
"As an instructor, I want to schedule directly from a recommended slot with automatic conflict checks so that I can publish quickly without risking double-bookings."
Description

Convert a recommended slot into a published class in one step with pre-filled details and automatic conflict checks against instructor availability, room resources, and existing bookings. Prevent double-bookings by locking the slot during confirmation, and provide smart alternatives if conflicts occur. On publish, trigger waitlist invites, payment links, calendar sync, and attendee notifications, and capture analytics for measuring impact on attendance and no-show rates.

Acceptance Criteria
One-Click Publish From Suggested Slot
Given I have selected a recommended slot from Global Time Finder and have a default class template configured When I click "One-Click Publish" Then a class is created with pre-filled title, instructor, duration, location/room or virtual link, capacity, price, and start/end times that match the suggested slot in the correct time zone And Then the class appears on the public booking page and internal schedule within 5 seconds of confirmation And Then the operation is atomic: either the class is fully published or a clear error is returned with no partial records created And Then an audit log entry is recorded with user_id, slot_id, timestamp, and outcome
Conflict Guard: Instructor, Room, and Existing Bookings
Given I initiate publishing for a selected slot When automatic conflict checks run Then the system verifies instructor availability across internal and synced external calendars, honoring travel/buffer rules, and checks assigned room/resource availability and overlapping classes/events And Then the check completes within 1.5 seconds for 95th percentile And Then if any conflict exists within the slot plus buffer, publishing is blocked and a conflict summary lists the resource, conflicting event title, start/end, and source calendar And Then if no conflicts are found, publishing proceeds without manual overrides
Slot Locking During Confirmation
Given I open the publish confirmation for a recommended slot When the confirmation modal opens Then the system places a lock on the (instructor, room/resource, start_time, end_time) tuple for up to 2 minutes (TTL) And Then concurrent attempts to publish or book the same tuple receive a "Temporarily locked" message and cannot proceed until the lock is released And Then the lock is released on cancel, on publish failure, or automatically at TTL expiry, whichever occurs first And Then locks are idempotent per tuple and session, survive transient network errors, and no orphan locks remain after server restart
Smart Alternatives on Conflict
Given a conflict prevents publishing the selected slot When I request alternatives Then at least 3 viable alternative slots within ±7 days are returned that satisfy instructor and room availability, respect quiet-hour rules, and avoid known high no-show windows And Then alternatives are sorted by Predicted Conversion Score (descending) and display rationale badges (e.g., Peak Demand, Low No-Show) And Then alternatives load within 2 seconds and each can be one-click published directly And Then if fewer than 3 alternatives exist, the UI displays the count available and offers an option to broaden criteria
Waitlist Invites and Payment Links on Publish
Given the class publishes successfully and waitlist automation is enabled When invites are triggered Then the top N eligible waitlisted contacts for the matching class/program are invited where N = min(capacity, waitlist size) And Then each invite includes a unique single-use payment link that expires after 30 minutes by default, with statuses Sent, Opened, Paid, Expired tracked And Then successful payments decrement capacity atomically; once capacity is reached, remaining links are automatically voided and invitees are notified And Then invite dispatch begins within 15 seconds of publish and completes within 2 minutes for up to 500 invites
Calendar Sync and Stakeholder Notifications
Given the class is published When calendar sync and notifications are enabled Then events are created/updated on linked instructor and room calendars (Google/Outlook/ICS) with accurate title, location, conferencing link, and time zone within 60 seconds And Then the instructor and studio admin receive confirmation via email and in-app notification within 30 seconds containing class details and manage links And Then if any calendar sync fails, the publish still succeeds, failures are retried with exponential backoff, and affected users receive a failure notice with remediation steps
Analytics Capture for One-Click Publish
Given a class is published via One-Click from a suggested slot When the publish operation completes Then an analytics record is created with fields: class_id, slot_id, user_id, source="Global Time Finder", flow="one_click_publish", decision_time_ms, conflict_check_duration_ms, lock_duration_ms, automations_triggered[], and timestamp And Then the record is queryable in analytics within 2 minutes and retained for at least 13 months And Then subsequent attendance and no-show outcomes are automatically linked to this record for impact measurement

Timeproof Invites

Sends .ics calendar invites and updates with full VTIMEZONE/UTC data so Google, Apple, and Outlook render the exact same local start time. Auto‑updates on reschedules and includes join links—ending calendar drift for traveling clients and hybrid classes.

Requirements

Standards-Complete ICS Generation with VTIMEZONE/UTC Parity
"As a client booking a class, I want the calendar invite to show the exact local start time regardless of where I am so that I never miss class due to timezone or DST errors."
Description

Generate RFC 5545-compliant .ics files for every booking that include VEVENT with persistent UID, DTSTAMP, and SEQUENCE; DTSTART/DTEND specified using IANA TZID plus corresponding Zulu (UTC) timestamps; and an embedded VTIMEZONE component with full standard/daylight observances so Google, Apple, and Outlook render the same local start time across regions and DST changes. Populate ORGANIZER, ATTENDEE, SUMMARY, LOCATION, and DESCRIPTION fields, support RRULE/EXDATE for recurring classes, apply correct line folding and CRLF, and set METHOD appropriately (REQUEST for new/updates). Ensure files are under size limits, are cache-safe, and carry consistent branding from ClassTap.

Acceptance Criteria
New Single Booking ICS — Core VEVENT and METHOD Initialization
Given a new confirmed booking is created in ClassTap When the .ics file is generated Then the VCALENDAR contains VERSION:2.0, PRODID beginning with -//ClassTap//, and METHOD:REQUEST And the VEVENT contains a globally unique, stable UID that persists across regenerations And DTSTAMP is present in Zulu (UTC) format representing the generation time And SEQUENCE is set to 0 for the first issuance And DTSTART and DTEND are present with TZID set to a valid IANA time zone (e.g., America/Los_Angeles) And an embedded VTIMEZONE component is included matching the TZID with STANDARD and DAYLIGHT observances And SUMMARY, DESCRIPTION, LOCATION, ORGANIZER, ATTENDEE, and URL properties are populated And the file validates with no errors against an RFC 5545 validator
Cross-Client Time Parity — VTIMEZONE + UTC Correspondence
Given an event scheduled in a DST-observing IANA time zone within the next 12 months When the .ics is imported into Google Calendar (web), Apple Calendar (macOS/iOS), and Microsoft Outlook (Windows/M365) Then each client renders the same local start and end times as intended for the specified TZID And the VEVENT includes DTSTART;TZID=<IANA TZID> and DTEND;TZID=<IANA TZID> And the VCALENDAR embeds a VTIMEZONE component whose STANDARD/DAYLIGHT rules correspond to the IANA TZID for the event date And the VEVENT includes X-CLASSTAP-UTC-START and X-CLASSTAP-UTC-END properties in Zulu (UTC) that match the UTC instant implied by the TZID-based DTSTART/DTEND And no client shifts the event time after sync or across subsequent refreshes
Recurring Series With Exceptions — RRULE/EXDATE Correctness
Given a recurring class defined with RRULE (e.g., FREQ=WEEKLY;BYDAY=MO,WE;COUNT=10) and two exception dates When the .ics is imported and the series is expanded by a standards-compliant library Then the total number of generated instances equals the RRULE expansion minus the EXDATEs And instances fall on the specified BYDAY set And EXDATE removes the specified occurrences with no duplicates And if a single occurrence is modified, a separate VEVENT override is included with the same UID and a RECURRENCE-ID matching the original instance datetime And all instances reference the same VTIMEZONE and maintain correct local times across DST changes
Organizer/Attendees, Branding, and Join Link Population
Given a booking (virtual, in-person, or hybrid) with a known instructor and registered attendees When the .ics file is generated Then ORGANIZER is populated with a mailto: address and a CN matching the instructor or studio name And each attendee is represented by an ATTENDEE property with mailto:, CN (name), CUTYPE=INDIVIDUAL, ROLE=REQ-PARTICIPANT, RSVP=TRUE, and PARTSTAT=NEEDS-ACTION for initial invites And SUMMARY includes the class title and retains ClassTap branding via PRODID and/or X-WR-CALNAME And DESCRIPTION contains a clickable join URL for virtual/hybrid sessions and a manage-booking link And LOCATION contains a full physical address when applicable; for hybrid, LOCATION indicates venue plus “Online” and the join URL remains in DESCRIPTION And URL points to the ClassTap booking page for the event
Update and Cancellation Flows — UID Persistence and SEQUENCE Increment
Given an existing event has its time or location changed When a new .ics is issued for the same booking Then METHOD is REQUEST And UID remains identical to the original event And SEQUENCE is incremented by 1 relative to the last sent value And DTSTAMP is updated to the new generation time in UTC And attendees retain their identifiers; only changed fields are updated And major clients (Google, Apple, Outlook) update the existing calendar entry rather than creating a duplicate Given an event is cancelled When a cancellation .ics is sent Then METHOD is CANCEL and VEVENT includes STATUS:CANCELLED with the same UID and latest SEQUENCE And clients remove or mark the event as cancelled
Formatting, Size Limits, and Cache-Safe Delivery
Given an .ics file is generated for any booking or series When inspecting the raw file and HTTP response Then all lines are folded at 75 octets with proper CRLF line endings and continuation whitespace And text fields escape commas, semicolons, and backslashes per RFC 5545 And Content-Type is text/calendar; charset=utf-8 and the filename ends with .ics via Content-Disposition And a single-event .ics is <= 200 KB and a recurring series up to 52 instances is <= 1 MB And the download endpoint serves over HTTPS with Cache-Control: public, max-age=300, plus ETag and Last-Modified headers And a subsequent GET with If-None-Match or If-Modified-Since returns 304 Not Modified when content is unchanged; on changes, ETag/Last-Modified update consistently with the incremented SEQUENCE
Automatic Invite Updates for Reschedules and Cancellations
"As an instructor, I want calendar invites to auto-update when I reschedule or cancel a class so that attendees’ calendars always reflect the latest details without manual emails."
Description

Automatically send calendar updates whenever a booking is edited, rescheduled, or canceled in ClassTap by reissuing the same UID with incremented SEQUENCE and appropriate METHOD (REQUEST for updates, CANCEL for cancellations). Preserve attendee list, propagate changes to SUMMARY, DTSTART/DTEND, LOCATION, DESCRIPTION, and conference details, and ensure clients replace rather than duplicate events. Support series-level updates vs single-occurrence changes for recurring bookings and handle waitlist-to-payment transitions that convert holds into confirmed invites. Provide idempotent re-send logic and guardrails to avoid spam on rapid edits.

Acceptance Criteria
Single-Instance Reschedule Update
Given a confirmed single-instance booking with an initial .ics invite (UID U, SEQUENCE S) delivered to Google, Apple, and Outlook test accounts When the organizer changes the start time and/or updates location, description, or join link in ClassTap Then ClassTap issues an .ics update within 60 seconds with the same UID, METHOD:REQUEST, SEQUENCE > S, updated DTSTART/DTEND, LOCATION, DESCRIPTION, and join link, including VTIMEZONE and UTC And the ATTENDEE list and each attendee’s existing PARTSTAT values are preserved; ORGANIZER remains unchanged And importing the update into Google Calendar, Apple Calendar, and Outlook updates the existing event and results in 0 duplicate events across clients
Single-Instance Cancellation
Given a confirmed single-instance booking with an initial .ics invite delivered to attendees When the organizer cancels the booking in ClassTap Then ClassTap issues an .ics cancellation within 60 seconds with the same UID, METHOD:CANCEL, and SEQUENCE strictly greater than the last sent And importing the cancellation into Google, Apple, and Outlook removes the event with 0 residual duplicates or stale instances
Recurring Series — Single Occurrence Change
Given a recurring booking series (RRULE) with an initial series .ics invite delivered to attendees When the organizer edits only one occurrence (date/time and/or location/description) Then ClassTap issues an .ics update with METHOD:REQUEST containing a VEVENT with RECURRENCE-ID for the targeted instance in the correct TZID, same UID, and incremented SEQUENCE And importing the update in Google, Apple, and Outlook updates only that occurrence; all other instances remain unchanged; 0 duplicates are created
Recurring Series — Series-Level Update
Given a recurring booking series with at least one exception already present and an initial series invite delivered When the organizer applies a series-level change (e.g., location, description, or start time) Then ClassTap issues a master VEVENT update with METHOD:REQUEST, same UID, incremented SEQUENCE, updated fields, and preserved exceptions And importing the update in Google, Apple, and Outlook updates all non-exception future instances accordingly and results in 0 duplicates
Waitlist-to-Payment Promotion to Confirmed Invite
Given a class with an attendee currently on waitlist When the attendee is promoted to confirmed via payment (auto or manual) Then ClassTap sends an .ics invite with METHOD:REQUEST to the promoted attendee using the canonical UID (series or single), including VTIMEZONE/UTC, SUMMARY, DTSTART/DTEND, LOCATION, DESCRIPTION, and join link And no update emails are sent to other attendees; the promoted attendee sees exactly one calendar event after import (0 duplicates); any prior hold placeholder is replaced rather than duplicated
Idempotent Resend and Edit-Throttling
Given a booking where the organizer performs multiple edits within 30 seconds (e.g., time changed twice, description updated) When the changes are saved in rapid succession Then ClassTap coalesces updates to emit at most 1 .ics email per attendee per booking per 30-second window containing only the final state as of the last edit And SEQUENCE increases by exactly 1 for each emitted update; identical payload resends are suppressed; suppressed updates do not increment SEQUENCE And no attendee receives more than 3 update emails for the same booking in any 5-minute period
Cross-Client Timezone Consistency and No-Drift Rendering
Given attendees in different time zones, including one crossing a DST boundary before the event When the booking is rescheduled in ClassTap Then the .ics includes full VTIMEZONE for the event TZID and UTC timestamps as required so that Google, Apple, and Outlook render the same local start time per attendee And the measured drift between clients’ displayed start times is 0 minutes; no unintended date shifts occur; the event is not converted to all-day; DST status is correct on all clients
Join Link and Hybrid Location Embedding
"As a hybrid class attendee, I want the invite to include a working join link and the physical address so that I can easily join online or attend in person without searching for details."
Description

Embed join links and location details so events are actionable across calendar clients: include clickable URL in DESCRIPTION, populate LOCATION with venue or "Online"/hybrid label, and attach conferencing metadata where supported. Add dial-in numbers and access codes when available, and surface safety notes or door codes for in-person sessions. Ensure that updates to meeting links or rooms propagate via the same UID/SEQUENCE flow. Provide fallbacks so Apple, Google, and Outlook users can always access the join method with one tap.

Acceptance Criteria
Clickable Join Link Across Calendar Clients
Given an event with a join URL is exported as .ics When the .ics is imported into Google Calendar (web and Android), Apple Calendar (iOS and macOS), and Outlook (desktop, OWA, and mobile) Then the event displays a clickable join hyperlink in the event details in each client And clicking or tapping the hyperlink opens the exact join URL encoded in the .ics And the .ics includes both a URL property set to the join URL and the join URL within the first 200 characters of DESCRIPTION And no duplicate protocols are present and the URL matches the pattern ^https?://.+
Online-Only Event Location Rendering
Given an online-only class with a provider or room name When the .ics is generated Then LOCATION is set to "Online — {provider/room}" and contains no street address And DESCRIPTION begins with the join URL followed by concise joining instructions And Apple, Google, and Outlook do not render a map pin for the event location And the event remains actionable with a single tap on the join URL on iOS and Android
In-Person Venue With Safety Notes and Door Codes
Given an in-person class with a venue name, full postal address, and access notes or door codes When the .ics is generated and imported into Google, Apple, and Outlook Then LOCATION contains "Venue Name, Street, City, Region, Postal Code, Country" And tapping the LOCATION opens the native maps application to that address And DESCRIPTION includes an Access section containing safety notes and door codes, not stored in LOCATION And Access content is visible to attendees in all clients
Hybrid Event With Dual Access Paths
Given a hybrid class with both a physical venue and an online join URL When the .ics is generated and imported into Google, Apple, and Outlook Then LOCATION begins with "Hybrid — {Venue Name}" and includes the postal address And DESCRIPTION contains both "Join: {URL}" and "In-person: {address}" within the first screen of text And Apple Calendar opens maps from LOCATION and exposes a clickable join link in notes And if a client strips vendor metadata, the join URL is still visible and clickable in DESCRIPTION
Dial-In Numbers and Access Codes
Given conferencing details include dial-in numbers and an access code or PIN When the .ics is generated Then DESCRIPTION contains a Dial-in section listing at least one number in E.164 format with a country label and the access code And at least one tel: link is provided with post-dial digits (e.g., tel:+15551234567,,123456#) and is tappable on iOS and Android And if multiple numbers are available, up to three are included prioritized by organizer locale then by provider defaults And Outlook, Apple, and Google display the tel: links without truncation of the post-dial sequence
Vendor-Specific Conferencing Metadata for Native Join Buttons
Given the conferencing provider supports client-recognized .ics extensions When the .ics is generated for events using Microsoft Teams Then Outlook (desktop, OWA, and mobile) renders a native Join button for the event And the .ics includes the provider-specific join metadata required by Outlook in addition to the standard URL and DESCRIPTION And in clients that do not support the extension, the join remains accessible via the clickable URL
Update Propagation for Changed Join Links or Locations
Given an attendee previously added the event from a .ics with UID U When the organizer reschedules, changes the join link, or updates the venue and a new .ics is sent with the same UID and an incremented SEQUENCE and updated DTSTAMP Then Google, Apple, and Outlook update the existing event in place without creating a duplicate And the new join link and/or location replace the old values in the attendee's calendar And the attendee’s RSVP status is preserved after the update And previous join links no longer appear in the event body across all clients
Smart Timezone Resolution and Fallbacks
"As a traveling client, I want my invite to always reflect the class’s correct start time in my current calendar view so that changing my device timezone doesn’t cause calendar drift."
Description

Resolve the correct timezone for each attendee using profile preference, class default timezone, booking context, and geolocation hints at booking time. Persist the event’s TZID and include VTIMEZONE so that if a client travels, their calendar still renders the event at the correct local time for the class location. When timezone is unknown, default to the class’s TZID with UTC fallback in DTSTART/DTEND and include a note in DESCRIPTION. Allow attendees to override their timezone in account settings and ensure consistency across reminders and reschedules.

Acceptance Criteria
Per-Attendee Timezone Resolution Priority at Booking
Given an attendee with a profile timezone set When they complete a booking Then attendee_resolved_timezone equals the profile timezone And booking.meta.timezone_source equals "profile" Given profile timezone is unset and a timezone is supplied in the booking flow (browser or explicit selection) When the booking is completed Then attendee_resolved_timezone equals the supplied timezone And booking.meta.timezone_source equals "booking_context" Given neither profile nor booking_context timezone is present and geolocation returns a TZID When the booking is completed Then attendee_resolved_timezone equals the geolocated TZID And booking.meta.timezone_source equals "geolocation" Given none of the above produce a timezone When the booking is completed Then attendee_resolved_timezone equals class_default_TZID And booking.meta.timezone_source equals "class_default" And automated tests cover all branches and persist the selected TZID in the booking record
ICS Invite Anchored to Class Timezone with VTIMEZONE
Given a confirmed booking When generating the .ics invite Then VEVENT.DTSTART and VEVENT.DTEND include TZID={class_default_TZID} And a matching VTIMEZONE component for that TZID is included And VEVENT.UID is stable per booking, METHOD is REQUEST, ORGANIZER and ATTENDEE are populated, and URL contains the join link When importing the .ics into Google Calendar, Apple Calendar, and Outlook on devices set to three different timezones Then each calendar displays identical local start and end times matching the class location time
Traveling Client Sees Same Local Class Start Time
Given an attendee has accepted the .ics invite When they change their device timezone to a different region and view the event in Google, Apple, and Outlook Then the event start time remains equal to the class local start time And the event is not "floating" (DTSTART/DTEND include TZID; no VALUE=DATE floating times) And no duplicate events are created upon timezone change
Unknown Attendee Timezone Fallback for Communications
Given attendee_resolved_timezone is unknown at booking When sending confirmation and reminder communications Then times are displayed in class_default_TZID and include the UTC equivalent in parentheses (e.g., 10:00 AM PDT (17:00 UTC)) And the .ics invite uses TZID=class_default_TZID and includes a matching VTIMEZONE And DESCRIPTION contains the line: "If your calendar shows a different time, reference UTC: <timestampZ>" And QA verifies templates for Email and SMS include both times
Attendee Timezone Override Propagates Consistently
Given an attendee changes their timezone in Account Settings When they save the change Then the new TZID is persisted and used for all future communications and new bookings within 5 minutes And previously sent .ics invites are not regenerated And on any reschedule, the VEVENT UID remains the same, SEQUENCE increments, DTSTART/DTEND stay anchored to the class TZID, and email/SMS time formatting uses the attendee's new TZID And an audit entry records old_tzid, new_tzid, user_id, and timestamp
Reschedule Sends Correct ICS Update and Reminders Align
Given a class is rescheduled or its location timezone changes When the update is published Then an updated .ics is sent with the same UID, SEQUENCE incremented by 1, and DTSTART/DTEND anchored to the new class TZID and VTIMEZONE And Google, Apple, and Outlook update the existing event in place within 15 minutes (no duplicate events) And all reminder schedules recalculate to attendee_resolved_timezone and align to the updated class start time And the join link remains unchanged unless the class join URL changed; if changed, DESCRIPTION and URL fields are updated
Auditability and Error Handling for Timezone Resolution
Given any step of timezone resolution fails (e.g., tzdb mapping missing) When the booking proceeds Then the system defaults attendee_resolved_timezone to class_default_TZID And logs an error with correlation_id, booking_id, and resolution_path And the Admin booking detail shows a "TZ Fallback: Class Default" badge And DESCRIPTION of the .ics includes the UTC reference timestamp And unit tests verify the fallback behavior and presence of the user-visible note
Multi-Client Compatibility and QA Matrix
"As a product manager, I want confidence that invites behave the same across Google, Apple, and Outlook so that we eliminate discrepancies that cause missed classes."
Description

Validate invites across major calendar ecosystems—Google Calendar (web/Android/iOS), Apple Calendar (macOS/iOS), Outlook for Windows/Mac/Web, and Exchange/Office 365—covering DST transitions, recurring rules, cancellations, attendee updates, attachments, and line folding. Establish an automated test suite that generates golden .ics fixtures and snapshot-renders key fields, plus a manual QA checklist for edge cases (e.g., leap seconds ignored, overnight classes, attendees in different timezones). Document known client quirks and bake workarounds into the generator to ensure identical local times and behavior.

Acceptance Criteria
Cross-Client Local Time Parity on DST Transition (Single Event)
Given a VEVENT with DTSTART/DTEND in a specific TZID and an embedded VTIMEZONE that defines the correct STANDARD/DAYLIGHT offsets And attendees on Google Calendar (web/Android/iOS), Apple Calendar (macOS/iOS), and Outlook (Windows/Mac/Web with Exchange/Office 365) And the event occurs on a day crossing into or out of DST for either organizer or attendee timezone When the .ics invite (METHOD:REQUEST) is delivered and added by each client Then each client renders the same local start and end times as computed from the TZID rules (±0 minute deviation) And no client shifts the event by 1 hour or duplicates it on DST change And the UID is identical across clients and instances And the event remains anchored to the intended local time even if an attendee’s device timezone differs from the organizer’s
Recurring Series with Exceptions Render Consistently Across Clients
Given a weekly VEVENT series with RRULE (e.g., FREQ=WEEKLY;BYDAY=MO,WE), specific TZID with VTIMEZONE, and two exceptions using EXDATE And one occurrence rescheduled using RECURRENCE-ID and a distinct DTSTART When the series .ics is imported and updated across Google, Apple, and Outlook clients Then all base occurrences appear at the correct local times with no drift across the series And EXDATE instances do not appear on any client And the rescheduled instance replaces (not duplicates) the original occurrence on every client And SEQUENCE increments on updates, DTSTAMP updates, and all clients reflect the latest state within 5 minutes of receipt
Reschedule and Join-Link Update Without Duplicates
Given an existing single-instance event with a join URL in DESCRIPTION and/or URL fields and in a text/html ALTERNATE part when supported When the organizer reschedules the event by sending an updated REQUEST with the same UID and SEQUENCE incremented by 1 Then each client updates the original event’s time in place (no duplicate entries) And the join URL remains present and clickable in the event details on all clients And alarms/notifications adjust to the new start time according to each client’s default behavior (no orphaned reminders at the old time) And the organizer and all attendees see identical local start times after the update
Attendee RSVP and Role Updates Sync Across Ecosystems
Given an event invite with ORGANIZER and multiple ATTENDEE properties including ROLE, PARTSTAT, and RSVP When an attendee accepts, declines, or marks tentative from Google, Apple, or Outlook Then the organizer receives a response and the generator issues an updated .ics reflecting the new PARTSTAT for that attendee And all clients display the updated attendee status within their UI within 10 minutes And adding/removing an attendee via an update (REQUEST with incremented SEQUENCE) results in exactly one event for that attendee (no ghost events) And required vs optional attendee roles (ROLE=REQ-PARTICIPANT/OPT-PARTICIPANT) persist across clients
Single-Instance and Series Cancellations Apply Uniformly
Given a series and a single-instance event present on all target clients When the organizer sends METHOD:CANCEL for a single occurrence using RECURRENCE-ID Then only that occurrence is removed across clients and the remainder of the series persists unchanged When the organizer sends METHOD:CANCEL for the entire series or single-instance event Then the entire event/series is removed across all clients with no residual duplicates And attendees no longer see reminders for canceled items And cancellation notices reflect the correct local time and instance details
Attachments and RFC5545 Line Folding Are Compatible
Given an invite containing ATTACH properties (URL-based) and long text fields (SUMMARY, DESCRIPTION, LOCATION) requiring RFC5545 line folding When the .ics is parsed by Google, Apple, and Outlook clients Then all ATTACH URLs remain intact and clickable in event details (no truncation) And folded lines are unfolded correctly with no lost or merged characters And no client rejects the .ics due to line length; all lines are folded at <=75 octets with a single space prefix on continuations And DESCRIPTION text with URLs and join links remains complete and functional across clients
Automated Golden .ics Fixtures and Cross-Client Snapshot Tests in CI
Given a test harness that generates .ics files for single, recurring, rescheduled, canceled, and DST boundary cases across multiple TZIDs When the CI pipeline runs on push to main and nightly Then golden .ics fixtures are regenerated deterministically and diffed; any semantic change fails the build with a readable diff And snapshot-rendered key fields (SUMMARY, DTSTART/DTEND with TZID, VTIMEZONE offsets, UID, SEQUENCE, METHOD, RECURRENCE-ID, EXDATE, ATTENDEE PARTSTAT, ATTACH) match expected values And a manual QA checklist exists for edge cases (e.g., overnight crossing midnight, mixed timezones, known client quirks) with pass/fail recorded And documented client-specific workarounds (e.g., Outlook requiring TZID names, Apple handling of URL field) are tracked and validated by tests
Reliable Invite Delivery and Tracking
"As an instructor, I want reliable delivery and visibility into invite send status so that I can quickly resolve issues if a client didn’t receive the calendar event."
Description

Deliver invites via email using MIME multipart with text/calendar (method=request/cancel) and .ics attachment, signed with SPF/DKIM/DMARC for inbox placement, and provide a downloadable .ics link on the booking confirmation and dashboard. Implement retries, bounce handling, and rate limiting; log delivery status per attendee and expose a simple dashboard for instructors to see sent/failed invites and re-send when needed. Ensure idempotency keys prevent duplicate sends during transient errors and throttle updates to batch rapid successive edits.

Acceptance Criteria
Multipart Email Invite Delivery (REQUEST)
Given a confirmed booking with at least one attendee When an initial invite is triggered Then the outbound email is MIME multipart with a text/plain or text/html body, a text/calendar part with method=REQUEST, and a .ics attachment containing identical event data And the calendar part has Content-Type text/calendar; method=REQUEST; charset=UTF-8; name="invite.ics" And the ICS contains UID, ORGANIZER, ATTENDEE, DTSTART, and DTEND fields And a delivery log entry is created per attendee with status Queued or Sent and a provider messageId
Calendar Cancellation Delivery (CANCEL)
Given a booking with previously sent invites When the booking is canceled by the instructor Then an email is sent to each attendee containing a text/calendar part and .ics attachment with METHOD:CANCEL and the same UID as the original invite And the ICS SEQUENCE is incremented relative to the last sent invite/update And a delivery log entry is recorded per attendee with status Sent
Authentication and Inbox Placement (SPF/DKIM/DMARC)
Given outbound invite emails are sent from the configured sending domain When the invite is received by seed mailboxes at Gmail, Outlook.com, and iCloud Then Authentication-Results headers include spf=pass, dkim=pass, and dmarc=pass aligned to the visible From domain And the message includes a DKIM-Signature using a 2048-bit key that verifies successfully And no recipient server rejects the message due to authentication failure
Downloadable .ics Links on Booking and Dashboard
Given a user is on the booking confirmation page for a successful booking When they click the "Download .ics" link Then the response is HTTP 200 with Content-Type text/calendar and a filename ending in .ics And the ICS UID matches the UID used in the email invite for that attendee And the attendee and instructor dashboards each display a working "Download .ics" link for the booking
Retries, Bounce Handling, and Rate Limiting
Given tenant_rate_limit is configured to 200 emails per minute and a batch of 250 invites is queued When the system processes the batch within 60 seconds Then no more than 200 messages are sent in the first minute and at least 50 are queued with ETAs And transient 4xx or timeout responses are retried up to 5 attempts with exponential backoff of approximately 1, 2, 4, 8, and 16 minutes And hard bounces (5xx or definitive bounce classifications) stop retries and mark status Bounced with provider code and reason stored And soft bounces are retried per policy and final status is Delivered or Failed within 24 hours
Delivery Status Logging and Instructor Dashboard
Given invites are sent to multiple attendees for a class When the instructor opens the invite delivery dashboard Then each attendee row displays status in {Queued, Sent, Delivered, Deferred, Bounced, Complaint}, last-updated timestamp, and provider messageId And the dashboard allows filtering by class, date range, and status And clicking "Resend" creates a new send attempt and appends a new log entry linked to the prior attempt And failed entries display the last error code and reason text
Idempotency and Update Throttling
Given an idempotency key is generated per attendee per event change When duplicate send requests with the same idempotency key occur within 24 hours Then only one email is sent and subsequent requests return success without sending, referencing the original messageId in logs And when an instructor makes multiple edits to the same event within 60 seconds Then updates are batched and a single update email is sent per attendee with one incremented SEQUENCE value And no attendee receives more than one update email per 60 seconds for the same event
Instructor Controls and Branding Settings
"As a studio owner, I want to control the branding and defaults of calendar invites so that every event reflects our studio identity and provides the right information to clients."
Description

Add Timeproof Invites controls in ClassTap settings: toggle feature on/off per class type, configure organizer display name and reply-to address, set default reminder VALARM times, and customize event title/description templates with variables (class name, instructor, studio address, join link, refund policy). Provide per-class overrides and preview of the generated .ics and email, ensuring brand consistency and compliance with privacy settings. Changes should take effect on new sends and future updates while preserving past event history.

Acceptance Criteria
Toggle Timeproof Invites Per Class Type
Given I am an admin with access to Settings > Timeproof Invites And Class Types A and B exist When I turn the Timeproof Invites toggle OFF for Class Type A and ON for Class Type B and save Then sessions created or published after save for Class Type A do not send .ics invites or updates And sessions created or published after save for Class Type B send .ics invites containing VTIMEZONE with correct local start time and auto-send updates on reschedule And previously sent invites for any past sessions are not altered
Organizer Display Name and Reply-To Configuration
Given I set Organizer Display Name to "Studio XYZ" and Reply-To to "bookings@xyz.com" and save When a new invite is sent for any enabled class type Then the .ics ORGANIZER property contains CN=Studio XYZ and a mailto: address that does not expose personal emails per privacy settings And the outbound email's Reply-To header equals bookings@xyz.com And saving is blocked with an inline error when Reply-To is not a valid email format And when the account privacy setting "Hide personal email" is ON, no instructor personal email appears in the .ics or email headers
Default Reminder VALARM Times
Given I configure default reminders of 24 hours and 60 minutes before start and save When a new invite is generated Then the .ics contains two VALARM components with TRIGGER values -P1D and -PT60M relative to DTSTART And when I change the defaults to 30 minutes and save Then new invites and reschedule updates sent after the change include a single VALARM with TRIGGER -PT30M And previously sent invites are not altered
Template Customization With Variables
Given I set the event title template to "{{class_name}} with {{instructor}}" and the description template includes {{studio_address}}, {{join_link}}, and {{refund_policy}} When I preview using a sample class "Morning Yoga" taught by "Ava" Then the previewed title is "Morning Yoga with Ava" and the description renders the address, join link, and refund policy values And saving is blocked with a clear error if templates contain unknown variables (e.g., {{foo}}) And the same rendered content appears in the sent email body and the .ics DESCRIPTION for new invites
Per-Class Overrides
Given a scheduled class instance exists with global defaults configured When I enable Overrides on that class instance and change the title, reminders, and organizer name Then only that class instance's new invite and any future updates use the override values And removing the override reverts the instance to the current defaults And other class instances remain unaffected
Live Preview of .ics and Email
Given I am editing Timeproof Invites settings or a class instance override When I click Preview for a selected class date and timezone Then the .ics preview includes a VTIMEZONE component matching the class timezone and DTSTART/DTEND that reflect the correct local time and UTC offset for that date (including DST) And the email preview shows the final event title, description, organizer display name, and join link as they will be delivered
Change Propagation and History Preservation
Given there are sent invites for past and future sessions When I change any Timeproof Invites setting (toggle, organizer/reply-to, reminders, templates) and save Then past sessions' invites and calendar entries are not modified And future sessions with invites already sent receive an update (.ics METHOD:REQUEST with incremented SEQUENCE) reflecting the new settings And future sessions created after the change use the new settings on first send

QuietHour Reminders

Schedules SMS/email reminders to land within each attendee’s preferred local hours and respects do‑not‑disturb windows. Messages clearly display the user’s local start time and auto‑adjust for DST shifts, improving show‑up rates without overnight pings.

Requirements

Local Timezone Resolution & Normalization
"As an attendee, I want reminders to reflect my actual local time so that I receive messages at convenient hours and see the correct class start time."
Description

Determine and persist each attendee’s accurate local timezone using a prioritized resolution strategy: explicit profile timezone, device/browser settings when available, phone country/area metadata, recent login IP geolocation (privacy-respecting), and class/studio location as a last-resort fallback. Normalize to IANA identifiers, handle ambiguous regions, and re-resolve on significant signals (e.g., new login from distant region) without spamming users. Expose timezone on attendee profiles and via API, cache results, and maintain audit trails for changes. Provide deterministic conversions for scheduling and message rendering, with guardrails for stale or conflicting sources. Integrate with booking flows, waitlist promotions, and reminder templates to ensure times are computed in the attendee’s local context.

Acceptance Criteria
Profile Timezone Takes Priority
Given an attendee has set a profile timezone to "America/Chicago" and device/browser reports "America/Los_Angeles", when computing the attendee local time, then the system uses "America/Chicago". Given a user enters a non-IANA label (e.g., "CST" or "Central Time"), when saved, then the value is normalized to a valid IANA identifier and persisted. Given a user clears their profile timezone, when computing local time thereafter, then the next priority source is used.
Multi-Source Resolution and Priority with Guardrails
Given no profile timezone and a valid device/browser timezone is available, when resolving timezone, then the device/browser timezone is selected. Given no profile or device timezone and phone country/area metadata maps to a single timezone, when resolving timezone, then the mapped IANA timezone is selected. Given phone country/area metadata maps to multiple possible timezones, when resolving timezone, then the phone metadata is ignored and the next source is considered. Given IP geolocation is permitted and accuracy radius <= 100 km and higher-priority sources are unavailable, when resolving timezone, then the IP-derived timezone is selected; otherwise it is skipped. Given no higher-priority sources are available, when resolving timezone, then the studio location timezone is used as last-resort fallback. Given a new candidate timezone conflicts with the current timezone but has equal or lower priority within a 7-day stability window, when resolving, then the current timezone is retained.
Significant Location Change Triggers Re-Resolution
Given the stored timezone is "America/New_York", when the user logs in from an IP geolocated >800 km away and in a different timezone, then the timezone is re-resolved to the new IANA timezone. Given a timezone re-resolution occurs, when persisting, then an audit log entry is created with old_timezone, new_timezone, source, timestamp, and reason "significant_location_change". Given multiple significant signals occur within 24 hours, when evaluating re-resolution, then at most one timezone update is applied per 24 hours per attendee. Given reminder jobs scheduled for future delivery exist, when the attendee timezone changes, then all future reminder schedules are recalculated to the attendee’s new local quiet hours without sending additional notifications about the change.
Deterministic Local Time Rendering and DST Handling
Given a class start at "2025-03-10 18:00 America/Los_Angeles", when rendering for an attendee in "America/New_York", then the displayed time is "2025-03-10 21:00" with the attendee’s timezone label. Given a DST transition creates an ambiguous or nonexistent local time, when scheduling reminders, then the system uses IANA transition rules to select the correct occurrence and avoids scheduling at nonexistent times. Given the same input datetime, timezone, and format, when converting multiple times, then the output is identical across runs and services. Given waitlist promotion or booking confirmation messages are generated, when determining send time, then the attendee’s local timezone is used and quiet-hour windows are respected.
Profile and API Exposure
Given an authenticated user views an attendee profile, when fields load, then the timezone is displayed as a valid IANA identifier with a human-friendly label. Given an authorized API client requests attendee details, when the response is returned, then it includes timezone.iana_id, timezone.source, and timezone.last_updated fields. Given a user submits a profile timezone update via UI or API, when the value is not a valid IANA identifier or recognized alias, then the request is rejected with a validation error; when valid, then it is saved and visible via profile and API within 5 seconds. Given an unauthorized user or token requests attendee details, when the response is returned, then timezone fields are omitted per access controls.
Caching, Performance, and Auditability
Given repeated timezone conversions for the same attendee within a 15-minute window and no new signals, when requests are processed, then cache hit rate is >= 95% and p95 conversion latency is <= 50 ms. Given a timezone update is persisted, when subsequent conversions occur, then related cache entries are invalidated within 5 seconds. Given any timezone create/update event, when auditing data is queried, then an audit record exists including actor (system or user), considered_sources, chosen_source, old_value, new_value, timestamp, and request_id, retained for at least 12 months.
Ambiguity, Privacy, and Fallback Handling
Given an input timezone label alias (e.g., "PST", "Central Time") is provided, when normalizing, then it maps to a single IANA identifier; if unmappable, then the input is rejected. Given a user has opted out of IP-based geolocation in privacy settings, when resolving timezone, then IP geolocation is not used. Given conflicting signals oscillate within 7 days (e.g., alternating device and IP zones), when resolving, then the current timezone is retained unless the user explicitly overrides. Given no reliable sources are available, when resolving timezone, then the studio location timezone is used and marked with source "fallback". Given phone number country/area metadata conflicts with device/browser timezone, when resolving timezone, then phone metadata alone does not override an existing timezone.
Per-Channel Quiet Hours & DND Preferences
"As an attendee, I want to set when it’s okay to receive reminders via SMS or email so that I avoid overnight pings but still get timely notifications."
Description

Enable attendees to define preferred send windows and do‑not‑disturb periods per channel (SMS, email), including weekday/weekend variations, temporary blackout ranges (e.g., vacation), and minimum lead-time preferences (e.g., no messages after 9pm or before 8am, at least 12h before class). Provide sensible regional defaults for new users and allow studios to set account-level templates that attendees can override. Store preferences in a timezone-aware model, validate for overlaps/conflicts, and expose management via profile UI and one-click preference links in messages. Ensure preferences versioning, auditability, and API access for integrations.

Acceptance Criteria
Per-Channel Preference Management via Profile UI and One-Click Links
Given an authenticated attendee viewing Profile > Notifications, When they select SMS and Email tabs, Then they can configure for each channel: preferred send window(s), do-not-disturb window(s), weekday/weekend variants, and minimum lead-time hours. Given the attendee saves valid changes, When the request succeeds, Then the preferences persist and are immediately reflected in subsequent scheduling calculations. Given the attendee clicks a one-click preferences link from an SMS/email, When the token is valid and unexpired per configured TTL, Then they land on the channel-scoped preferences page pre-authenticated, can update settings, and save successfully. Given a one-click link is expired or already used, When it is opened, Then the system returns 401/expired state and offers sign-in to proceed without exposing current preferences.
Effective Preference Resolution: Attendee Override > Studio Template > Regional Default
Given a studio-level template exists and a regional default table is configured, When a new attendee is created under that studio, Then their initial effective preferences equal the studio template; if no template, then regional defaults apply. Given an attendee overrides any channel-specific setting, When effective preferences are queried, Then the attendee override takes precedence over the studio template, which takes precedence over the regional default, per setting and per channel. Given a studio updates its template, When attendees without overrides are evaluated, Then their effective preferences reflect the updated template; attendees with overrides remain unchanged. Given the scheduler or API requests preferences, When it resolves values, Then it returns both the resolved values and their source (attendee|studio|regional) for each field.
Timezone & DST-Safe Scheduling with Minimum Lead-Time
Given an attendee has timezone tzid set and channel preferences including preferred windows and a 12-hour minimum lead-time, When a class starts at 2025-03-12T18:00 local, Then the reminder send time is selected within an allowed local window that is no later than 2025-03-11T18:00 local. Given the next allowed window falls after the lead-time boundary, When computing send time, Then the system selects the latest allowed time before the boundary; if none exists, it skips sending and logs the reason "no allowed window before lead-time". Given a DST transition date (spring forward/fall back), When computing send times, Then all comparisons are performed in the attendee’s local time, honoring tzid and ensuring the chosen time falls within configured local windows. Given a reminder is sent, When composing the message, Then the class start time is formatted in the attendee’s local time with timezone abbreviation adjusted for DST on the event date.
Weekday vs Weekend Quiet Hours and DND Enforcement
Given distinct weekday and weekend preferred windows per channel, When selecting a candidate send time, Then the system uses the recipient’s local day-of-week of the candidate send time to apply the correct set (weekday for Mon–Fri, weekend for Sat–Sun). Given do-not-disturb windows are defined, When evaluating a candidate send time, Then any time falling within a DND window is disallowed even if it is inside a preferred window. Given multiple allowed windows exist in a day, When the first window is blocked by DND, Then the scheduler evaluates subsequent windows on that day before moving to prior days to meet lead-time constraints.
Temporary Blackout (Vacation) Ranges Suppress Sends
Given an attendee sets a blackout from 2025-12-20T00:00 to 2025-12-27T23:59 in their local time, When scheduling reminders whose candidate send times fall within that interval, Then the system suppresses those sends for all channels covered by the blackout. Given a reminder would normally send during the blackout, When seeking an alternative time, Then the scheduler picks the latest allowed time before the blackout start that still satisfies minimum lead-time; if none exists, the reminder is skipped and the skip is auditable with reason "blackout".
Conflict Detection and Validation on Save
Given a user configures overlapping or invalid time ranges within a channel (start >= end, duplicate or overlapping DND entries, overlapping blackout ranges), When they attempt to save, Then the system blocks the save and displays inline errors identifying the conflicting fields and ranges. Given the union of preferred windows minus DND across a full week yields zero allowed minutes, When saving, Then the system blocks the save with an error indicating no deliverable windows remain for that channel. Given minimum lead-time is set above 168 hours (full week) and allowed windows occur only within the last 168 hours before classes by policy, When validating, Then the system warns/block per validation rules and provides guidance to reduce lead-time or expand windows.
API Access with Versioning and Auditability
Given an integration calls GET /v1/attendees/{id}/preferences, When the attendee exists, Then the API returns 200 with the current resolved and raw per-source values, tzid, and current version id. Given an integration calls PUT /v1/attendees/{id}/preferences with valid changes, When the request passes validation, Then the API returns 200/204, persists changes, increments version id, records an audit entry (timestamp, actor/app id, method UI/API/Link, source IP), and emits a preferences.updated webhook. Given a PUT includes invalid or conflicting ranges, When processed, Then the API returns 400 with machine-readable error codes and fields. Given an auditor requests GET /v1/attendees/{id}/preferences/versions, When versions exist, Then the API returns a chronological list of versions with diffs and metadata; when reverting via POST /versions/{versionId}/revert, Then the system restores values and creates a new version documenting the revert.
Window-Constrained Scheduling Engine
"As a studio owner, I want reminders delivered within each attendee’s allowed hours without missing intended reminder times so that show-up rates improve without disturbing customers."
Description

Implement a scheduling service that computes the next permissible send time per recipient by intersecting reminder timing rules (e.g., 24h and 2h before class) with each attendee’s allowed windows and legal restrictions. Queue and dispatch messages accordingly, deferring those that fall outside windows and promoting urgent ones when within grace thresholds. Support de-duplication, message coalescing (e.g., combine multiple reminders for the same day), idempotent retries, rate limiting, and backoff. Ensure horizontal scalability, high availability, and strong observability (metrics, traces, dead-letter queues). Integrate with SMS and email providers, respect per-channel throughput, and guarantee ordering where relevant. Provide configuration for cutoffs, grace periods, and exception policies.

Acceptance Criteria
Next Permissible Send Time — Window Intersection
Given an attendee with timezone America/New_York and allowed window 08:00–21:00 local And reminder rules of 24h and 2h before class And a class at 2025-09-15T10:00:00-04:00 When the scheduler computes send times Then the 24h reminder is scheduled at 2025-09-14T10:00:00-04:00 and the 2h reminder at 2025-09-15T08:00:00-04:00 And both scheduled_at timestamps fall within 08:00–21:00 local And scheduled UTC timestamps correctly convert from the computed local times And jobs are enqueued with recipient_id, channel, rule_id, and scheduled_at populated
Legal Quiet Hours Enforcement
Given locale=US with legal quiet hours 21:00–08:00 local And an attendee with custom allowed window 07:00–22:00 local And a class at 2025-09-16T09:00:00-04:00 with rules 24h and 2h When computing send times Then the 2h target 2025-09-16T07:00:00-04:00 is not scheduled because it violates legal quiet hours And it is deferred to 2025-09-16T08:00:00-04:00 (start of legal window) since that is before class start And a compliance_reason="legal_quiet_hours" is recorded on the job And no messages are sent between 21:00–08:00 local for this locale
DST-Aware Scheduling and Local Time Rendering
Given an attendee in America/Los_Angeles with allowed window 08:00–21:00 local And a class at 2025-11-02T09:00:00-08:00 (post DST end) with rules 24h and 2h When computing send times across the DST transition Then the 24h reminder is scheduled at 2025-11-01T09:00:00-07:00 and the 2h reminder at 2025-11-02T07:00:00-08:00 And rendered message variables include class_start_local="9:00 AM PT" derived from the attendee timezone And if a computed local time falls into a nonexistent local time due to DST spring forward, it is shifted to the next valid minute within the allowed window boundary
Grace Thresholds for Urgent Reminders
Given grace configuration urgent_max_deviation=30m and standard_max_deviation=unbounded And attendee allowed window 08:00–21:00 local And a class at 2025-09-15T08:30:00-04:00 (2h target 06:30 outside window) When computing the 2h reminder Then because the nearest boundary 08:00 is +90m (>30m), the 2h reminder is suppressed with status=skipped and skipped_reason="beyond_urgent_grace" And the 24h reminder (target 2025-09-14T08:30:00-04:00) is scheduled at its target since it is within window Given attendee allowed window 06:30–21:00 local and a class at 2025-09-15T08:20:00-04:00 (2h target 06:20) When computing the 2h reminder Then it is promoted to 2025-09-15T06:30:00-04:00 (boundary within 10m ≤30m urgent grace)
Same-Day Coalescing and De-duplication
Given a recipient with two classes on 2025-09-15 at 10:00 and 12:00 local And a day-of reminder rule targeting 07:00 local And coalescing enabled with window=60m per recipient-channel When scheduling day-of reminders Then a single message is scheduled at 2025-09-15T07:00:00 local containing both class summaries And any duplicate jobs for the same recipient-channel-template-scheduled_at share an idempotency_key and only one send occurs And the audit log records coalesced_children with the merged job ids
Idempotent Retries, Backoff, and Dead-Lettering
Given a job dispatched to the provider returns timeouts/500s And retry policy {max_attempts:5, backoff:exponential, base=2, min=30s, max=10m} When retries are performed Then at most one delivery is performed per recipient-channel-idempotency_key And retry delays follow 30s, 60s, 120s, 240s, 480s (capped ≤10m) And after the 5th failed attempt the job is moved to the DLQ with last_error, attempt_count, and trace_id recorded And DLQ items are replayable via an operator action or automated policy without creating duplicate sends
Rate Limiting, Throughput, and Ordering Guarantees
Given SMS channel rate_limit=120 messages/min (rolling window) and provider concurrency=10 And two jobs for the same recipient-channel scheduled at 08:00:00 and 08:01:00 local When dispatching across multiple workers Then global SMS send rate never exceeds 120 messages in any rolling 60s interval And for the same recipient-channel, messages are sent in scheduled_at order (08:00 before 08:01) And on provider 429 with Retry-After, dispatch backs off accordingly without violating the channel rate limit or reordering And email channel dispatch respects its own independent rate_limit and does not affect SMS pacing
DST & Calendar Shift Handling
"As an attendee, I want my reminder to show the correct start time even when daylight saving changes so that I arrive on time."
Description

Accurately handle daylight saving time and other calendar anomalies by computing send times and rendering class start times with the attendee’s timezone at send-time. Address edge cases like skipped or repeated hours, hemisphere differences, and transitions occurring between scheduling and dispatch. Display localized time with clear zone abbreviations and 12/24h formatting per locale. Maintain canonical UTC timestamps with timezone context snapshots for auditability, and include tests around DST boundaries. Ensure ICS attachments and calendar links reflect correct local start times.

Acceptance Criteria
Spring Forward: Skipped Hour Send-Time Computation
Given an attendee in a DST-observing timezone where clocks advance by 1 hour, and the class local start occurs after the spring-forward transition When a reminder is configured for 1 hour before start Then the send-time is event_utc minus 60 minutes, maps to the correct pre-transition local wall time with the correct zone abbreviation, and exactly one message is dispatched Given a reminder configured 24 hours before such a class When the system computes the send-time Then the send-time is event_utc minus 24 hours; the displayed local wall-time difference may be 23 or 25 hours due to DST, but the elapsed real time is exactly 24 hours, and only one message is dispatched
Fall Back: Repeated Hour Disambiguation
Given a timezone that repeats the 01:00 hour at fall-back and a class start after the transition When a reminder is configured 1 hour before start Then the computed send UTC maps to a local time within the repeated hour with the correct abbreviation (e.g., EDT or EST) matching the UTC instant, and the reminder orders correctly in timelines by UTC Given rendering of the reminder time in UI and logs When the local time is within the repeated hour Then the time is displayed with the correct zone abbreviation to disambiguate and no duplicate or skipped sends occur
Send-Time Timezone Snapshot & Audit Trail
Given any reminder dispatch When the message is sent Then the system persists an audit record containing: event_utc, send_utc, attendee_tzid (IANA), effective_utc_offset_at_send, dst_in_effect flag, attendee_locale, rendered_local_start_time, rendered_local_send_time, and TZDB version used Given the stored audit record When the system re-renders using the snapshot Then the same rendered local times and abbreviations are reproduced deterministically
Per-Locale 12/24h and Zone Abbreviation Rendering
Given attendee locale en-US at send-time When rendering the class start time in the reminder Then the time uses 12-hour format with AM/PM and includes the correct short zone abbreviation (e.g., PST/PDT) Given attendee locale en-GB or fr-FR at send-time When rendering the class start time in the reminder Then the time uses 24-hour format and includes the correct zone abbreviation; if unavailable, a GMT±HH:mm offset is shown Given rendering at a DST boundary When the abbreviation is produced Then it matches the effective offset at send-time (e.g., CET vs CEST)
ICS Attachments and Calendar Links Reflect Correct Local Start Times
Given an attendee’s timezone and a class that occurs near a DST transition When generating an ICS attachment Then DTSTART/DTEND include TZID matching the attendee’s IANA timezone and a VTIMEZONE component so that importing into major calendars renders the correct local start time Given Add-to-Calendar links for Google, Apple, and Outlook When the attendee imports/opens the event Then the calendar shows the same local start time and zone abbreviation as rendered in the reminder at send-time Given automated tests across DST boundaries When ICS files and links are validated Then Google Calendar, Apple Calendar, and Outlook each display the correct localized start time
Cross-Hemisphere and No-DST Region Handling
Given attendees in Australia/Sydney (southern hemisphere), America/New_York (northern hemisphere), and Africa/Johannesburg (no DST) When computing reminder send-times and rendering class start times across their respective DST seasons Then each attendee sees correct local times, offsets, and abbreviations, and the send UTCs are correct; no spurious overnight sends occur due to misapplied hemisphere rules
Recompute at Dispatch for Post-Schedule Transitions
Given a reminder scheduled before a DST transition and set to send after the transition When the system determines the dispatch instant Then it uses the latest IANA timezone rules and the attendee’s current timezone and locale at send-time to render local strings, while preserving the canonical send UTC computed from event_utc minus the configured offset Given the attendee changes their preferred timezone between scheduling and send When the reminder is dispatched Then the message displays the class start time in the updated timezone, and the audit log captures both the prior and current tzids with the snapshot used at send
Message Preview & Send-Time Estimator
"As a studio owner, I want to preview when each reminder will send and how it appears in local time so that I can trust the system and adjust settings as needed."
Description

Provide an admin-facing tool to preview reminder content and see the calculated send time for a specific attendee and class, factoring in quiet hours, DND, lead times, legal restrictions, and DST. Allow simulation of different settings (e.g., changing quiet hours or reminder offsets) and show reasons when a message is deferred or coalesced. Surface warnings for potential conflicts and show fallback channels when one is blocked. Enable export of previews for support and client communication.

Acceptance Criteria
Preview shows localized content and start time
Given an admin selects a specific class session and attendee When the preview loads Then all template variables (e.g., attendee first name, class name, location) are resolved without placeholders And the class start time is displayed in the attendee’s local timezone with correct DST adjustment and timezone abbreviation And the preview clearly labels the timezone used for rendering And the calculated send time is shown adjacent to the preview using ISO 8601 with local human-readable format
Estimator honors quiet hours, DND, legal window, and lead time
Given a reminder offset relative to class start and configured quiet hours, DND window, and jurisdictional legal send window for the attendee’s locale When the send-time estimator is executed Then the returned send time is the earliest time that satisfies all active constraints and the requested lead time And if the original offset violates any constraint, the result is marked as deferred and each violated constraint is listed with a human-readable reason code And the output includes: final send timestamp (ISO 8601 with offset), source offset, constraint list (ordered by impact), and whether the message is deferred And if the calculation crosses a DST boundary, the final time reflects the correct post-shift local time
Simulation updates outcomes when adjusting settings
Given the admin modifies quiet hours and/or reminder offset in the simulator When the admin clicks Simulate Then the estimator recomputes within 1,000 ms and the UI displays the new send time And the UI shows a diff between previous and new results, including changed reasons/constraints And the admin can revert to the original settings and results with a single action
Coalescing explanation for overlapping reminders
Given multiple reminders for the same attendee and class would produce sends within the configured coalescing window or the same deferred slot When the estimator runs Then only a single delivery is scheduled within that window And the preview displays a Coalesced status including the list of merged reminders (IDs/offsets and their original planned times) and the selected final send time And the reasons include a coalesced entry with details of why consolidation occurred
Fallback channel surfaced when primary is blocked
Given SMS delivery is blocked for the attendee due to DND, legal restrictions, or lack of consent When the preview and estimator are displayed Then SMS is labeled as Blocked with the specific reason(s) And the highest-priority available fallback channel (per configuration) is shown with its own estimated send time and constraints And if no channels are available, the UI shows No deliverable channel along with all blocking reasons and a link to consent/settings management And blocked channels do not display a send time
Export preview for support and client communication
Given an admin clicks Export on the preview screen When the export is generated Then a downloadable file is available within 5 seconds in PDF and CSV formats And the export includes: message body preview, calculated send time (ISO 8601 and human-readable local), applied constraints and reasons, coalescing details, fallback channel decisions, attendee and class identifiers, timezone used, and simulator inputs And sensitive contact fields are masked (phone last 4, email local-part partially masked) And the exported content exactly matches what is shown in the UI at the time of export
Validation and error handling for incomplete data
Given the attendee profile lacks a timezone When the estimator runs Then the class location timezone is used and a non-blocking warning is shown And if neither attendee nor class timezone is available or valid, the estimator shows an error state and disables export And if required legal policy data for the attendee locale is missing, the estimator applies the configured global default policy and surfaces a warning indicating the fallback policy used
Compliance & Consent Enforcement
"As a business owner, I want the system to enforce consent and legal quiet hours so that I avoid fines and maintain trust."
Description

Enforce per-region messaging regulations (e.g., TCPA, CASL, GDPR) by tracking per-channel consent, double opt-in for SMS, honoring STOP/START keywords, and providing clear unsubscribe mechanisms in emails. Apply jurisdiction-specific quiet hour restrictions even when user preferences are more permissive. Maintain compliant audit logs (timestamp, source, IP) and suppression lists synced with providers. Block sends when consent is missing or hours are restricted, and surface compliance reasons in logs and previews. Provide data subject request tooling and configurable retention policies.

Acceptance Criteria
SMS Double Opt‑In Enforcement per Region
Given an attendee provides a mobile number in a jurisdiction requiring SMS double opt‑in And initial consent is captured via a ClassTap form specifying channel=SMS and purpose=reminder When the system evaluates eligibility to send a reminder SMS Then send exactly one confirmation SMS within permitted hours requesting explicit opt‑in And block all other SMS until the attendee confirms via YES/START reply or confirmation link And mark SMS consent as Pending until explicit confirmation is received And upon confirmation, persist consent with timestamp, IP, source, user agent, jurisdiction, channel, and purpose And if confirmation is not received within 30 days, expire Pending consent and continue blocking SMS And if consent is revoked later, set status to Revoked and block future SMS And in previews and logs, display reason code "BLOCKED_MISSING_SMS_DOUBLE_OPT_IN" when blocked
STOP/START Keyword Handling and Provider Sync
Given the system receives an inbound SMS with STOP/STOPALL/UNSUBSCRIBE/CANCEL/END/QUIT from a subscribed number When the MO message is processed Then immediately set SMS consent to Revoked and add the number to the suppression list within 60 seconds And send a single opt‑out confirmation SMS per carrier guidelines And block all future SMS sends to that number within 60 seconds And sync the suppression status to the SMS provider within 60 seconds, retrying with exponential backoff up to 24 hours on failure And log timestamp, MO body, provider message ID, carrier, country code, and correlation ID Given the system receives START/UNSTOP/YES from a suppressed number in a region requiring double opt‑in Then set consent to Pending and re‑issue confirmation; only set to Granted after explicit confirmation Given the system receives HELP Then reply with business name, help URL/email, and STOP instructions
Jurisdiction Quiet Hours Override User Preferences
Given an attendee’s local timezone and jurisdiction‑specific quiet hour rules are determined And the attendee’s personal QuietHour preference is more permissive than the jurisdiction rule When a reminder send time is computed Then schedule delivery only within the jurisdiction‑allowed window And if the computed time is outside the window, defer to the next allowed window before event start And if no allowed window exists before event start, do not send and record reason "BLOCKED_QUIET_HOURS" And in message preview, display the evaluated timezone, jurisdiction rule ID, next delivery time, or block reason And ensure waitlist‑to‑payment automations also respect jurisdiction quiet hours And persist the decision and rule references in audit logs
DST‑Aware Local Time Rendering and Scheduling
Given an attendee’s timezone and locale are known And a class start time occurs near a DST transition When composing reminder content and scheduling send time Then render the start time in the attendee’s local time with correct DST offset and timezone abbreviation (e.g., Tue Mar 12, 7:00 AM EDT) And align the reminder’s scheduled send to the intended local offset even on DST change days And for non‑existent local times (spring forward), adjust to the next valid minute and log adjustment reason "DST_NON_EXISTENT_TIME_ADJUSTED" And for ambiguous times (fall back), select the post‑transition occurrence and log reason "DST_AMBIGUOUS_TIME_RESOLVED" And provide automated tests covering at least two DST regions and one non‑DST region validating rendering and scheduling
Email Unsubscribe and Suppression List Synchronization
Given a marketing reminder email is generated When delivered to a recipient Then include a clearly visible one‑click unsubscribe link and List‑Unsubscribe and List‑Unsubscribe‑Post headers And when the unsubscribe link is activated, mark the recipient unsubscribed within 60 seconds and show a confirmation page And add the email to the suppression list and sync with the ESP within 60 seconds (with retries up to 24 hours on failure) And prevent future marketing emails while allowing transactional reminders if permitted by policy Given a transactional reminder email is generated Then include a manage preferences link and business contact information And do not include marketing content in transactional emails And process bounces and spam complaints into suppression within 60 seconds and log the provider event IDs
Comprehensive Compliance Audit Logging
Given any send decision (sent, deferred, blocked) for SMS or email is made When the decision occurs Then write an immutable audit record including timestamp (UTC), actor (system/user), recipient identifier, channel, message type, consent state, jurisdiction, quiet‑hour evaluation, decision outcome, reason codes, and correlation ID And for consent/opt‑in/out events, also store source IP and user agent And expose logs in the UI with filters by recipient, date range, channel, decision, and reason code And enable CSV/JSON export with access controls and data minimization for non‑admin roles And enforce retention policy on audit logs; upon expiry, anonymize or delete and log the retention action with timestamp
Data Subject Requests and Retention Policy Enforcement
Given a verified data subject request (access/export, rectification, deletion) is submitted When the request is queued Then verify identity, acknowledge receipt, and complete the request within 30 days And for access/export, provide a machine‑readable export including personal data and consent history with timestamps and sources And for deletion, remove or anonymize PII and retain a hashed suppression token to prevent future messaging; propagate deletions to providers and record confirmations And pause all non‑required communications for the subject while deletion is pending And enforce configurable retention periods per region/purpose; run daily purge jobs to delete/anonymize expired data and write purge events to the audit log And support legal hold exceptions with documented reason and expiry, preventing purge until hold release
Waitlist-to-Payment Quiet Hour Integration
"As a waitlisted attendee, I want promotion messages to arrive during my allowed hours with enough time to pay so that I don’t miss a spot or get disturbed overnight."
Description

Integrate quiet hour logic into automated waitlist promotions. When a spot opens, schedule the promotion and payment link within the recipient’s allowed window. Provide configurable hold durations that can span DND periods and extend holds appropriately to ensure fairness. Implement escalation for imminent class start times (e.g., switch channel, shorten hold with clear notice) while respecting compliance. Prevent double-bookings by atomically reserving spots during holds. Track and report conversion rates and time-to-claim segmented by quiet hour adherence.

Acceptance Criteria
Promotion Scheduled Within Attendee Quiet Hours
Given a waitlist spot opens and the attendee’s allowed contact window is currently open, When the system triggers the promotion, Then the message is sent within 5 minutes of trigger. Given a waitlist spot opens outside the attendee’s allowed contact window, When the next allowed window begins, Then the message is sent within 5 minutes of window start. Given the attendee’s DND window spans midnight, When scheduling the message, Then the message is not sent during DND and is scheduled for the first minute after DND ends. Given the attendee has no explicit quiet hours configured, When scheduling promotions, Then the system uses the org default window (e.g., 08:00–21:00 local) and records the default usage in the audit log. Given the attendee’s timezone is known, When rendering scheduled send time, Then all internal scheduling is stored in UTC and converted from the attendee’s IANA timezone. Given a rate limit or provider delay occurs, When a scheduled send time would fall outside the allowed window, Then the message is deferred to the next allowed window and no message is sent during DND.
Hold Duration Extends Across DND Windows
Given a promotion is sent and a hold length of 60 minutes is configured, When any portion of the 60 minutes would occur during the attendee’s DND, Then the hold expiry is extended so the attendee receives a full 60 minutes of active time within allowed hours. Given a promotion is scheduled but not yet delivered due to DND, When delivery occurs at the next allowed window, Then the hold start time is the delivery timestamp, not the spot-open timestamp. Given the hold is extended across multiple consecutive DND windows, When calculating expiry, Then the system accumulates extensions until the attendee has received the full configured hold length. Given the hold expiry is extended, When displaying timers in messages or UI, Then the expiry shows the extended time in the attendee’s local time and matches the backend hold record.
Atomic Reservation Prevents Double-Booking During Holds
Given a spot becomes available and a hold is placed for Attendee A, When any other attendee attempts to purchase the same spot, Then the purchase is rejected until A’s hold expires or is released. Given concurrent claim attempts from multiple attendees within 100ms, When the system processes the claims, Then exactly one successful purchase is recorded and no oversell occurs. Given Attendee A completes payment during the hold, When any pending holds exist for the same spot, Then those holds are canceled and affected attendees are notified of unavailability within 2 minutes. Given a hold expires, When the next ranked waitlist attendee is eligible, Then the system schedules the next promotion according to that attendee’s quiet hours within 5 minutes of expiry.
Imminent Class Start Escalation While Respecting Quiet Hours
Given class start is within 120 minutes (org-configurable) and a spot opens, When the attendee’s SMS quiet hours are active, Then the system sends an email or push immediately and schedules SMS for the next allowed window. Given class start is within 120 minutes and the configured hold length would extend past class start, When sending the promotion, Then the system shortens the hold to 15 minutes (org-configurable minimum) and includes “Hold expires at HH:MM local” in the message. Given escalation was applied, When auditing the event, Then the chosen channel, shortened hold, and rationale are recorded. Given compliance restrictions prohibit contacting via a specific channel during quiet hours, When evaluating escalation, Then that channel is not used and only compliant channels are selected.
Local Time Rendering and DST Accuracy
Given an attendee is in a timezone that will shift for DST between send and class start, When the system displays start and hold expiry times, Then the times reflect the correct post-shift local time with the proper abbreviation (e.g., PDT/PST). Given all scheduling is stored in UTC, When converting to display for messages and UI, Then conversions use the attendee’s IANA timezone and pass unit tests for DST start and end cases across US/EU/AU regions. Given the attendee’s timezone is missing, When scheduling and rendering, Then the class timezone is used as fallback and the fallback usage is captured in the audit log.
Quiet-Hour Segmented Reporting
Given promotions have been sent for waitlist-to-payment, When viewing the reporting dashboard, Then conversion rate and median time-to-claim are shown segmented by quiet-hour adherence (in-window vs deferred), channel used, and escalation applied. Given a date range, class, or instructor filter is applied, When refreshing the report, Then metrics update within 5 seconds and reflect data with freshness under 15 minutes. Given a user exports the report, When downloading CSV, Then each promotion row includes attendee ID, timezone used, adherence segment, channel, send timestamps (UTC and local), hold length, claim timestamp, and outcome. Given dashboard totals, When reconciling to promotion logs for the same filters, Then counts differ by no more than 0.5%.

Deposit Dial

Set tiered or percentage deposits by offer with clear deadlines and auto‑conversion into the first installment at start. Expiring deposit holds release seats to the waitlist automatically, lowering commitment friction while protecting cash flow and reducing manual follow‑ups.

Requirements

Offer-Level Deposit Rules
"As a studio owner, I want to set deposit rules per class so that I can lower commitment friction while protecting cash flow."
Description

Provide per-offer deposit settings supporting fixed amount, percentage of price, and tiered matrices by seat type, time-to-start, and customer segment. Define deposit due windows (absolute date or relative offsets) and policies (refundable, non-refundable, applies to balance). Auto-validate against price changes, discounts, vouchers, and taxes; compute deposit totals with currency rounding. Expose settings in the Offer editor with previews and guardrails (min/max deposit). Persist configuration and versioning for audit; apply rules to existing bookings per effective date without breaking commitments. Surface deposit terms at checkout and in invoices. Provide CRUD APIs and webhooks for deposit rule lifecycle events.

Acceptance Criteria
Offer Editor Configuration: Fixed and Percentage Deposits with Preview and Guardrails
Given an offer is open in the Offer Editor with a defined price and currency When the user selects deposit type "Fixed Amount" and enters a value within currency minor units and configured min/max guardrails Then the Save action is enabled and the preview displays the per-seat deposit equal to the entered amount in the offer currency Given the user selects deposit type "Percentage" and enters a value within configured min/max percentage guardrails and precision limits When the preview renders Then the deposit shown equals price × percentage with currency rounding applied and validation passes Given a deposit value violates guardrails (e.g., percentage > max, fixed < min, or fixed > price) When the user attempts to save Then the save is blocked and inline validation messages identify each violated guardrail Given valid deposit settings When the user saves and reopens the Offer Editor Then the previously saved settings and preview values load identically
Tiered Deposit Matrix by Seat Type, Time-to-Start, and Customer Segment
Given a tiered matrix is configured across seat types, time-to-start buckets, and customer segment tags with a defined default tier When a shopper with segment S selects seat type T and the event is D days from start Then the engine selects a single tier using deterministic precedence SeatType > Segment > TimeBucket, else falls back to the offer default, and computes the deposit accordingly Given two tiers would overlap for the same SeatType/Segment/TimeBucket combination When the merchant attempts to save the matrix Then validation fails with an error listing the specific overlapping tiers Given a seat type lacks a matching tier and no default is defined When saving the matrix Then validation fails with a missing-default error for that seat type Given a booking contains multiple seats with different types When computing the total deposit Then the system applies each seat’s applicable tier and sums the per-seat deposits
Deposit Due Windows: Absolute Date and Relative Offset Availability
Given a deposit rule with an absolute due date/time in the offer’s timezone When current time is before the due timestamp Then checkout offers a "Pay Deposit" option and displays the due timestamp Given the same absolute due rule When current time is after the due timestamp Then checkout does not offer "Pay Deposit" and requires full payment Given a deposit rule with relative due window "N days before start" When booking occurs earlier than start_at minus N days Then checkout offers "Pay Deposit" and displays due = start_at minus N days at 23:59 in the offer timezone Given the same relative due rule When booking occurs after start_at minus N days Then checkout does not offer "Pay Deposit" and requires full payment Given any due window with potential daylight-saving transitions When the due timestamp is computed Then the system resolves the timestamp using the offer’s canonical timezone to avoid ambiguity
Deposit Terms at Checkout and Invoices; Auto-Application to Balance at Start
Given a deposit policy marked refundable When the shopper views checkout and the order confirmation/invoice Then the deposit line item is labeled "Refundable" and the terms text is displayed in both checkout and the invoice Given a deposit policy marked non-refundable When the shopper views checkout and the order confirmation/invoice Then the deposit line item is labeled "Non‑refundable" and the terms text is displayed in both checkout and the invoice Given a booking with a collected deposit that applies to the balance When the offer start time is reached Then the system automatically applies the deposit as the first installment, reduces the outstanding balance accordingly, and records the application in the invoice and transaction ledger Given a refundable policy and the booking is canceled before start per policy When a refund is initiated Then the system allows refund of the deposit amount according to policy rules
Pricing Dynamics: Discounts, Vouchers, Taxes, and Currency Rounding
Given a valid discount or voucher is applied to the booking When computing the deposit Then the deposit base equals the discounted subtotal before tax, deposit amount is derived per rule (fixed or percentage), applicable taxes on the deposit are calculated per jurisdiction, and the final deposit is rounded to the currency’s minor unit using standard rounding Given a voucher reduces the effective price to zero When computing the deposit Then the deposit amount is zero and no positive authorization is attempted Given the offer price changes after a deposit has been collected on an existing booking When recalculating balances Then the original deposit amount remains honored, the outstanding balance is recomputed from the new total less the deposit, and if the deposit exceeds the new total the overage is flagged for refund or credit Given a booking includes multiple seats possibly with different discounts When computing the total deposit Then the per-seat deposit is computed on each seat’s effective price with rounding applied per seat, and the total deposit equals the sum of per-seat deposits
Versioning, Effective Dates, and Auditability Without Breaking Existing Bookings
Given deposit rules support versioning with effective_from timestamps and version identifiers When a new version is published with effective_from = T Then bookings created on or after T use the new version, and bookings created before T continue to use their original version Given an auditor requests configuration history When retrieving deposit rule history via UI or API Then all versions are listed with editor identity, timestamps, change summaries, and effective ranges Given a rollback is required When a prior version is re-published with a new effective_from Then it becomes active for future bookings without modifying existing bookings’ commitments Given changes are saved as a draft When the draft is not yet published Then no bookings are affected and no webhooks are emitted
APIs and Webhooks for Deposit Rule Lifecycle
Given valid API credentials with appropriate scope When calling POST /offers/{offerId}/deposit-rules with a valid payload Then the API responds 201 with the created rule version and a deposit_rule.created webhook is emitted with offerId and version metadata Given an invalid payload that violates guardrails or creates tier overlaps When calling POST or PUT on the deposit-rules endpoint Then the API responds 422 with machine-readable error codes and no webhook is emitted Given DELETE is called on an active rule When the request succeeds Then the rule is marked deprecated (not applied to new bookings), existing bookings remain unaffected, and a deposit_rule.deprecated webhook is emitted Given GET /offers/{offerId}/deposit-rules?include=history is called When the request succeeds Then the response includes the active version and all historical versions with statuses and effective ranges
Auto-Release to Waitlist
"As a studio owner, I want seats to auto-release and cascade offers to the waitlist when deposit windows expire so that classes fill without manual follow-up."
Description

When a deposit payment or claim window expires, automatically release the reserved seat and sequentially offer it to waitlisted customers. Generate exclusive claim links with countdown timers, preventing double-booking across concurrent claims. Update capacity atomically and propagate availability to schedules, widgets, and calendars. Support configurable grace periods, number of waitlist passes, and lockout rules. Log all transitions and outcomes for support and analytics. Handle edge cases such as simultaneous expiries, multi-seat orders, and capacity changes.

Acceptance Criteria
Seat Auto-Release on Deposit Expiry
Given a class seat is held by a deposit with an expiry timestamp and an optional grace period When the expiry time plus configured grace period is reached and the release job/trigger runs Then the seat is released within 60 seconds, the hold status changes to "Released - Expired", capacity increments by 1 atomically, the customer can no longer access the held seat, and an audit event is recorded with correlation_id
Sequential Waitlist Offers With Exclusive Claim Links and Lockout Rules
Given a seat is released and a waitlist exists with admin settings: claim_window_minutes=M, max_passes=P, lockout_days=D When the release occurs Then the top waitlist candidate is sent a unique, single-use claim link that expires in M minutes and is bound to their user_id and offer_id And the claim page displays a server-synchronized countdown timer and disables claim actions after expiry And while the offer is active, the seat is soft-locked to that offer; normal booking flows cannot consume it And if the candidate declines or the offer expires, the next candidate is offered, incrementing pass_count, up to P total passes And candidates who reach P passes are placed in lockout for D days and are auto-skipped in subsequent offer cycles And all offer sends, views, claims, declines, expiries, and skips are logged with timestamps
Concurrency and Double-Booking Prevention Across Concurrent Claims
Given multiple claim links may exist due to multiple released seats or simultaneous offers When two or more candidates attempt to claim the last available seat concurrently Then the system performs an atomic capacity check and decrement inside a single transaction/lock And exactly one claim succeeds; all others receive a non-chargeable "Offer no longer available" response And capacity never goes below zero and no duplicate bookings are created And retries of the same claim link are idempotent and cannot create additional bookings
Configurable Grace Period Enforcement
Given a deposit hold has expired at time T and grace_period_minutes=G is configured (G can be 0) When current_time reaches T+G Then the seat becomes eligible for auto-release and waitlist offer processing And before T+G the seat remains held and no waitlist offer is sent And changes to G apply to pending (not yet released) holds immediately and are recorded in the audit log
Availability Propagation to Schedules, Widgets, and Calendars
Given a capacity change occurs due to a release or a successful waitlist claim When the transaction commits Then the public schedule view, embeddable widgets, and internal roster reflect the new availability within 5 seconds And API responses immediately return the updated availability post-commit And outbound webhooks and ICS feeds reflect the change on their next delivery/poll cycle and include correlation_id
Comprehensive Transition and Outcome Logging
Given any state change related to release and waitlist (release, offer_sent, offer_viewed, claim_attempted, claim_succeeded, claim_failed, declined, expired, skipped, locked_out) When the event occurs Then an immutable audit record is written with event_type, timestamp_utc, actor (user_id or system), class_id, order_id (if applicable), offer_id, claim_link_id, old_status, new_status, capacity_before, capacity_after, and correlation_id And support users can filter logs by class_id and correlation_id and export within 2 clicks And PII is minimized (user_id and hashed email only); full email/phone are not stored in logs And error events include error_code and trace reference
Edge Cases: Simultaneous Expiries, Multi-Seat Orders, and Capacity Changes
Given multiple deposit holds for the same class expire at the same second When the release processor runs Then releases are handled deterministically (e.g., by created_at ascending) with atomic capacity updates per seat and without race conditions And for a multi-seat order with k held seats expiring, k seats are released, and subsequent waitlist offers respect max_passes and claim windows And for multi-seat waitlist claims, the system reserves up to the requested seats atomically; if fewer seats remain, the claimant is offered the available count, and overselling is prevented And if capacity is reduced below confirmed+held, no new offers are issued until capacity becomes available; if capacity increases, pending candidates are offered immediately
Deposit Auto-Conversion & Balance Scheduling
"As an instructor, I want deposits to automatically convert into the first installment at class start so that I don’t have to manually reconcile balances."
Description

On the configured trigger (e.g., class start or plan start), automatically convert paid deposits into the first installment, update invoices, and schedule or capture the remaining balance per payment plan rules. Respect cancellation/reschedule policies, credits, and tax recalculation. Provide safeguards to avoid duplicate captures and idempotent retries for failures with dunning controls. Show a payment timeline on the booking detailing deposit receipt, conversion, and remaining installments.

Acceptance Criteria
Auto-Convert Deposit at Trigger
Given a confirmed booking with a paid deposit and a conversion trigger set to the class start time in the event’s timezone When the start timestamp is reached Then the deposit is converted into the first installment within 5 minutes, the installment reflects the applied amount, the invoice is updated, and receipts are sent to customer and admin And if the deposit exceeds the first installment amount, the excess is automatically applied to subsequent installment(s) on the same booking And if the deposit is less than the first installment amount, the remaining amount on that installment remains due per the plan and will be captured/scheduled per auto-charge settings And the conversion event is idempotent using a unique key per booking so it can only occur once
Schedule Remaining Balance per Payment Plan
Given a payment plan with defined installment counts, amounts or percentages, due date offsets, anchor date, and auto-charge setting And a booking whose deposit has successfully converted When the conversion is posted Then remaining installments are generated with due dates anchored to the plan’s anchor (e.g., class start) in the event’s timezone and amounts that sum exactly to the remaining balance after conversion And any available customer account credits are applied to reduce the remaining balance before scheduling And if auto-charge is enabled and a default payment method exists, payment intents are created and captures are attempted at each due date window (±10 minutes) And if auto-charge is disabled or no default payment method exists, pay-by-link requests are sent within 5 minutes of schedule creation And if a due date is already in the past at the moment of conversion, the system attempts capture immediately and updates the schedule accordingly And all scheduled amounts and dates are reflected on the booking and invoice
Cancellation Before Conversion Respects Policy
Given a booking with a paid deposit and a future conversion trigger And the customer cancels before conversion When the cancellation is processed Then the system prevents conversion, cancels any scheduled installments, releases the seat per policy, and applies the deposit per policy (refund or account credit) within 24 hours And if policy specifies a non-refundable deposit, the deposit is retained and the invoice reflects no further balance due And no capture attempts are made after cancellation
Reschedule Adjusts Conversion and Schedule
Given a booking with a paid deposit and conversion not yet triggered When the customer reschedules to an allowed new start date per policy Then the deposit is moved to the new booking/session, the conversion trigger updates to the new start time, and the installment schedule is recalculated and re-anchored to the new dates And any payment intents for the original schedule are canceled and no duplicate charges occur And confirmations are sent reflecting the updated schedule and amounts
Tax Recalculation on Conversion
Given a booking subject to tax When the deposit converts to the first installment Then taxes are recalculated on the transaction per current tax rules for the event location and customer tax status, and invoice tax lines are updated And rounding deltas are distributed across remaining installments so total tax collected equals the recalculated amount And if the deposit included provisional tax, an adjusting entry reconciles provisional versus final tax at conversion
Duplicate Prevention, Idempotent Retries, and Dunning
Given any installment capture attempt (including captures scheduled after conversion) When transient failures (network errors, timeouts) occur Then the system retries with exponential backoff (1h, 24h, 72h) up to 3 attempts using the same idempotency key, ensuring only one successful capture can post per installment And when hard declines occur, no automatic retry is attempted; a dunning sequence starts within 15 minutes per configured cadence, and the installment is marked Failed And concurrent duplicate webhooks or job retries do not create extra captures or duplicate schedule entries And an immutable audit log records each attempt and outcome
Payment Timeline Shows Deposit, Conversion, and Installments
Given a booking with a deposit and a payment plan When the customer or admin views the booking details Then a payment timeline displays entries for Deposit Received, Deposit Converted, and each Installment with status (Scheduled, Processing, Paid, Failed, Canceled), amounts, timestamps in the event’s timezone, payment method, and links to receipts And timeline updates within 30 seconds of payment events And the timeline is accessible (screen-reader friendly, status color contrast meets WCAG 2.1 AA) and exportable to CSV
Deadline & Claim Notifications
"As a student, I want clear reminders and payment links before my deposit deadline so that I don’t lose my seat."
Description

Deliver branded, templated notifications via email, SMS, and push for deposit due, nearing deadline, expiration, seat released, waitlist claim window, conversion, and receipts. Include dynamic tokens (amount, deadline with time zone, policy links) and secure payment links. Support per-offer templates, quiet hours, opt-outs, and localization. Track sends, opens, clicks, and conversions, and provide admin preview and test send.

Acceptance Criteria
Deposit Due Notification: Per-Offer Template, Dynamic Tokens, and Secure Links
Given an offer has channel-specific templates (email, SMS, push) and branding configured And a recipient has a pending deposit due for that offer When the system triggers a Deposit Due notification Then the template selected matches the offer and channel And all dynamic tokens render correctly, including {amount} with currency, {deadline} with recipient’s time zone, and {policy_link} And the payment link is HTTPS, signed, and time-bound to the deposit deadline And the message includes brand name and visual branding per channel (logo/colors for email; brand prefix for SMS; app name for push) And if a channel-specific template is missing, the system uses the default template for that channel and logs the fallback And clicking the payment link opens to the secure checkout for the correct pending deposit
Quiet Hours Enforcement Across Channels
Given quiet hours are configured at the organization level And a recipient’s time zone is known or inferred When a notification would be sent during quiet hours for the recipient Then the send is deferred to the next permissible window outside quiet hours for each channel And the scheduled send time reflects the recipient’s time zone And a send-deferred event is logged with original trigger time and rescheduled time And no message is transmitted during quiet hours
Opt-Out Respect and Suppression Logging
Given a recipient has opted out of one or more channels When a notification is generated for that recipient Then the system does not send on opted-out channels And the system attempts remaining available channels per the channel priority rules And if all channels are opted out, the notification is suppressed And a suppression record is logged with reason=opt-out and the affected channels And email/SMS templates include required unsubscribe/STOP instructions where applicable
Localization and Timezone-Aware Tokens
Given an offer has localized templates and a default locale And a recipient has a preferred locale and time zone When a notification is sent Then the system selects the template matching the recipient’s locale, else falls back to the offer default And amounts are formatted per locale (currency symbol/placement and decimals) And dates/times in tokens (e.g., {deadline}) are rendered in the recipient’s time zone with locale-appropriate format And policy links resolve to the locale-specific URL when available, else to default
Scheduling for Nearing Deadline and Expiration Notices
Given an offer defines notification offsets for nearing deadline and expiration And a recipient has an active deposit hold with a set deadline When the scheduler evaluates pending notifications Then nearing-deadline notifications are queued for the configured offsets before the deadline, adjusted for recipient time zone and quiet hours And an expiration notification is sent immediately upon deadline lapse if payment is not completed And all scheduled and sent events are timestamped and auditable And no duplicate notifications for the same event are sent within a defined idempotency window
Waitlist Seat Release and Claim Window Notifications
Given a deposit hold expires and the seat is released And a waitlist exists for the offer When the system identifies the next eligible waitlisted recipient Then a Claim Window notification is sent with a secure, time-bound claim/payment link And the claim window duration is reflected correctly in the message using the recipient’s time zone And if the claim window lapses unclaimed, the next waitlisted recipient is notified, and the process repeats And opt-outs and quiet hours are honored at each step, with suppressions and deferrals logged
Admin Template Preview and Test Send
Given an admin selects an offer and channel template When the admin opens Preview Then a preview renders with sample or selected recipient data showing all dynamic tokens resolved, localization applied, and branding visible And when the admin performs a Test Send to specified addresses/numbers/devices Then the test notification is delivered using sandbox/sample data without altering production states And the system logs the test send with channel, recipient, and rendered payload for audit
Admin Overrides & Adjustments
"As a studio admin, I want to override deposit terms when exceptions arise so that I can resolve customer issues without breaking the system."
Description

Enable authorized staff to override deposit amounts, extend or shorten deadlines, manually release or assign seats, waive or refund deposits (full or partial), and apply credits. Require reason codes and capture an audit trail with before/after values, actor, timestamp, and affected booking. Enforce role-based permissions and display conflict warnings (e.g., oversell risk) before committing. Ensure all actions reconcile inventory, invoices, and ledgers consistently.

Acceptance Criteria
Override Deposit Amount With Audit Trail
Given an authorized admin with Manage Deposits permission opens a booking with an existing deposit And selects a reason code When the admin overrides the deposit amount to a new value Then the system validates the new amount is between 0 and the total booking amount and does not result in a negative remaining balance And recalculates the remaining balance and installment schedule accordingly And updates any scheduled deposit auto-conversion at class start to use the new amount up to the remaining balance And creates an itemized invoice adjustment reflecting the override And posts balanced ledger entries for the adjustment with references to booking and invoice And writes an audit record capturing before and after values, actor, timestamp, reason code, optional note, and booking ID And makes the change visible immediately on the booking timeline and invoice And does not change inventory counts And if validation fails, the system shows an error and no changes are saved
Adjust Deposit Deadline and Auto-Conversion
Given an authorized admin with Manage Deposits permission opens a booking with an active deposit hold And selects a reason code When the admin updates the deposit payment deadline to a specific timestamp Then the system validates the timestamp and that it is within the allowed window for the offer And updates the hold expiration to the new deadline And reschedules notifications and payment reminders to align with the new deadline And updates the deposit auto-conversion schedule at class start accordingly And writes an audit record capturing before and after deadline values, actor, timestamp, reason code, optional note, and booking ID And updates invoice terms to reflect the new due date without duplicating charges And if the new deadline is in the past and the hold is unpaid, the system displays a confirmation that the seat will immediately expire and release to the waitlist; upon confirm the release occurs with all reconciliations and notifications; upon cancel no changes are saved
Manually Release Seat From Hold
Given an authorized admin with Manage Seats permission opens an unpaid deposit hold And selects a reason code When the admin chooses Release Seat and confirms Then the system displays a warning that the hold will be canceled and the next waitlisted attendee will be offered the seat And upon confirmation cancels the hold, frees one inventory unit, and updates capacity counts atomically And voids or reverses any pending deposit invoice items associated with the hold And posts balanced ledger reversals with references to booking and invoice And queues a waitlist offer to the next eligible attendee and sends notifications to affected parties And writes an audit record capturing action, before and after status, actor, timestamp, reason code, optional note, and booking ID
Manual Assign Seat From Waitlist With Conflict Warnings
Given an authorized admin with Manage Seats permission views the waitlist for a full session And selects a reason code When the admin assigns a seat to a specific waitlisted attendee Then the system checks current capacity and any seats reserved by in-progress payment sessions And if assignment would oversell and the admin lacks Override Capacity permission, the system blocks the action and shows a conflict warning without saving And if assignment would oversell and the admin has Override Capacity permission, the system shows a conflict warning requiring explicit confirmation before proceeding And upon successful assignment, inventory counts are updated atomically; if overriding capacity the booking is marked as a capacity override And a deposit request is generated or existing account credit is applied per offer rules And invoice and ledger entries are created consistently with the booking And notifications are sent to the attendee with payment instructions And an audit record is written with before and after values, actor, timestamp, reason code, optional note, and booking ID
Waive Deposit (Full or Partial)
Given an authorized admin with Manage Deposits permission opens a booking with an unpaid or partially paid deposit And selects a reason code When the admin waives a deposit amount Then the system validates the waive amount does not exceed the unpaid deposit And reduces the deposit due by the waived amount and adjusts the remaining balance accordingly And adds a Waived Deposit line item to the invoice And posts ledger entries moving the waived amount from deposit liability to a waiver expense account with references And updates any deposit auto-conversion at class start to reflect the new deposit due And writes an audit record with before and after values, actor, timestamp, reason code, optional note, and booking ID And if the deposit is fully paid, the system blocks waiving and instructs the admin to use Refund instead without saving changes
Refund Deposit (Full or Partial)
Given an authorized admin with Manage Refunds permission opens a booking with a paid deposit balance And selects a reason code When the admin initiates a refund for a specified amount and chooses refund to original payment or account credit Then the system validates the refundable amount does not exceed the paid deposit remaining after any applied conversions or prior refunds And creates a refund transaction with the chosen method and updates the payment gateway if applicable And updates the invoice to show the refund and new balances And posts balanced ledger entries reducing deposit liability and cash or increasing customer credit liability with references And sends a refund confirmation to the customer And writes an audit record with before and after values, actor, timestamp, reason code, optional note, and booking ID
Role-Based Permissions and Reason Codes Enforcement
Given a user attempts an override or adjustment action on deposits, deadlines, seats, or credits When the user lacks the required permission for the specific action Then the system prevents access to the action controls, returns a permission error on attempted execution, and makes no changes to inventory, invoices, or ledgers And records an audit entry of the denied attempt with actor and timestamp linked to the booking, without requiring a reason code And when the user has the required permission, the action flow requires a reason code selection before enabling the Confirm or Save action And the reason code list is configurable and the system blocks save if no reason code is selected
Payments, Compliance & Ledgers
"As a finance manager, I want deposit payments to be compliant and properly accounted for so that our books and payouts remain accurate."
Description

Process deposit transactions through supported gateways (e.g., Stripe, PayPal, Adyen) with tokenized, SCA-compliant flows. Support partial payments, multi-currency, tax-inclusive/exclusive pricing, fee absorption or pass-through, and instant refunds where available. Record double-entry ledger movements for deposit liabilities, conversions to revenue, refunds, and chargebacks to preserve cash flow clarity. Provide reconciliation exports and real-time webhooks for downstream accounting and CRM systems.

Acceptance Criteria
SCA Tokenized Deposit Checkout (Stripe/PayPal/Adyen)
Given Offer X uses Deposit Dial with a 30% deposit captured immediately and gateways Stripe, PayPal, and Adyen enabled When a learner completes checkout via Stripe with a card requiring SCA Then a 3DS2 challenge is performed via tokenized flow, no raw PAN is stored, and PaymentIntent status="succeeded" with captured=true within 5s And ledger posts: Debit Cash (Gateway Clearing)=deposit amount; Credit Deposit Liability=deposit amount When a learner completes checkout via PayPal with buyer approval Then the order is captured, a vault/billing-agreement token is stored, and no card data is stored by ClassTap And ledger posts: Debit Cash; Credit Deposit Liability equal to deposit amount When a learner completes checkout via Adyen requiring SCA Then a 3DS2 challenge is completed with resultCode in {Authorised} and capture follows configuration within 5s And gateway customer/payment method tokens and transaction IDs are stored for future charges And all requests/responses are redacted for sensitive fields and logs contain no PCI data (SAQ-A scope)
Multi-Currency and Tax Modes for Deposit Calculation
Given base price USD 100, tax rate 10%, deposit 25%, customer currency EUR, and taxes are Exclusive When the learner views checkout Then deposit amount equals 25% of USD 100 plus 10% tax on the deposit portion, converted to EUR using gateway FX rate at authorization time And all displayed amounts (subtotal, tax, total) match confirmation and receipt within ±0.01 EUR and follow ISO minor-unit rounding And ledger posts separate lines for: Credit Deposit Liability (net), Credit Tax Liability (tax), and Debit Cash (total) Given taxes are Inclusive When the learner views checkout Then the deposit is computed from tax-inclusive price with tax component correctly allocated and shown And currency codes and symbols match the customer currency across UI, receipt, and ledger
Fee Absorption vs Pass-Through on Deposits
Given gateway fee is 2.9% + $0.30 and Offer X is set to Merchant Absorbs Fees When a $30.00 deposit is captured Then the customer is charged $30.00 and ledger posts: Dr Cash $30.00; Cr Deposit Liability $30.00; fee recorded separately to Processing Fees Expense on payout reconciliation Given Offer X is set to Pass-Through Fees When a $30.00 deposit is captured Then the customer is shown and charged $30.00 + fee before confirmation with a labeled Processing Fee line item And ledger posts: Dr Cash = total charged; Cr Deposit Liability = $30.00; Cr Fee Income = fee amount And receipts and exports reflect fee treatment consistently with configuration
Deposit Liability Conversion to Revenue at Class Start
Given a booking with a captured deposit and Class Start time t0 When t0 occurs (or an authorized admin triggers conversion) Then the system posts ledger conversion: Debit Deposit Liability = deposit applied; Credit Revenue = same amount And emits webhook event deposit.converted within 60s including booking_id, amount, currency, ledger_entry_ids, gateway_transaction_id, and idempotency_key And the operation is idempotent: retried conversions do not create duplicate ledger lines or webhooks And the booking balance and payment schedule update to reflect the converted installment
Expiring Deposit Holds and Waitlist Auto-Charge
Given Offer X uses authorize-only deposits with a 24h hold and a waitlist is active When 24h elapses without capture Then the authorization is voided and the reserved seat is released And the next waitlisted learner with a stored token is auto-charged for the deposit; on success they are enrolled and receive confirmation within 1 minute And failures are retried up to 3 times with exponential backoff and alerts; subsequent failures move to the next waitlisted learner And ledger posts no entries for voids; successful auto-charge posts: Dr Cash; Cr Deposit Liability
Instant Refunds and Chargeback Handling
Given a captured deposit eligible for instant refund by the gateway When an admin initiates a refund before conversion Then the refund completes within 10s and ledger posts: Debit Deposit Liability; Credit Cash (refund) When a refund is issued after conversion to revenue Then ledger posts: Debit Revenue; Credit Cash (refund) When a chargeback/dispute webhook is received for the original payment Then the system creates reversal entries matching the disputed amount, marks booking Disputed, emits payment.disputed webhook, and blocks further auto-charges until resolved And each refund or dispute is linked to the original transaction via a reconciliation reference
Reconciliation Exports and Real-Time Webhooks
Given daily reconciliation exports are enabled for accounting and CRM When the export for a date range is generated Then it contains all double-entry ledger lines for deposits, conversions, refunds, chargebacks with columns: ledger_entry_id, booking_id, account, debit, credit, currency, fx_rate, gateway, transaction_id, created_at And account balances per account sum to zero (debits=credits) within each currency And files are delivered as CSV via secure download and optional SFTP, with record counts and SHA-256 checksum When payment/ledger events occur in real time Then signed webhooks (HMAC-SHA256, timestamped) are sent for payment.created, deposit.captured, deposit.converted, refund.created, dispute.received with retry and idempotency behavior (at-least-once, 24h retry window)
Deposit Performance Analytics
"As a studio owner, I want analytics on how deposits affect fill rates and revenue so that I can optimize tiers and deadlines for better outcomes."
Description

Provide dashboards and exports for deposit uptake, hold-to-pay conversion rate, waitlist claim rate, time-to-fill after release, no-show deltas versus non-deposit offers, revenue attributed to deposits, and refund/chargeback rates. Enable breakdowns by offer, instructor, cohort, and timeframe with filters for deposit type and deadline rules. Support A/B testing of deposit tiers and deadlines with statistically sound comparisons and flags for winning configurations.

Acceptance Criteria
Dashboard: Deposit Uptake by Offer and Timeframe
- Rule: Deposit uptake (%) = bookings initiated via deposit (paid deposit or deposit hold) ÷ all bookings initiated for offers where a deposit option was available, within the selected timeframe and active filters. - Given a user selects date range, offer(s), instructor(s), cohort(s), deposit type(s), and deadline rule(s); When the dashboard loads; Then the Deposit Uptake tile shows percent and counts per the rule. - Then breakdowns by offer, instructor, cohort, and time grain (day/week/month) display and their counts sum to the overall total; percent differences from overall are within 0.1pp due to rounding. - Then CSV export for this metric includes: date_grain, offer_id, offer_name, instructor_id, instructor_name, cohort_id, cohort_name, bookings_total, bookings_deposit, uptake_percent, and applies all active filters in the file header/context. - Performance: P95 load time for the tile and primary breakdown chart is ≤ 2.0s for up to 100k bookings in range.
Metric: Hold-to-Pay Conversion Rate
- Rule: Hold-to-pay conversion rate (%) = deposit holds that collected the first installment on/before the offer start datetime ÷ deposit holds created (seat reserved) within the selected timeframe and filters. - Then the metric displays numerator, denominator, and percent; a tooltip shows the exact formula and time basis (hold creation date). - Then breakdowns by offer, instructor, cohort, and time grain are available and totals reconcile to within 1 count of the overall due to timing boundaries at midnight. - If an A/B test on deposit tiers/deadlines is active, Then the metric shows variant-level results with p-value and 95% CI; "Winner" flag appears only when power ≥ 0.80 and p < 0.05 with ≥ 100 holds per variant. - Export includes: date_grain, offer_id, instructor_id, cohort_id, holds_created, holds_converted, conversion_percent, variant (if applicable).
Metric: Waitlist Claim Rate and Time-to-Fill After Release
- Rule: Seat release = seat freed due to deposit expiration or cancellation where a waitlist exists; Claim rate (%) = released seats subsequently purchased by any waitlisted user before class start ÷ total released seats; Time-to-fill = median and P90 duration from release timestamp to payment confirmation timestamp. - Then the widget shows claim rate (percent), released_seats, claimed_seats, median_time_to_fill, and P90_time_to_fill for the selected filters and date range. - Then a drilldown table lists releases with release_at, offer_id, cohort_id, claimed (Y/N), claim_user_id (masked), claimed_at, time_to_fill_minutes, and reason_for_release; counts in the table exactly match the aggregates. - Filters support offer, instructor, cohort, timeframe, deposit type, and deadline rules; breakdowns by offer and cohort are available. - Export includes the drilldown rows and an aggregates CSV; time_to_fill values are computed in minutes with ISO8601 timestamps.
Comparison: No-Show Delta vs Non-Deposit Offers
- Rule: No-show rate = no-show bookings ÷ attended+no-show bookings for the selected group; Delta (pp) = deposit-group no-show rate − matched non-deposit baseline no-show rate, matched on instructor and timeframe filters. - Then the card displays deposit no-show rate, non-deposit baseline rate, delta in percentage points, and 95% CI; if either group has < 50 bookings, shows "Insufficient data" state and hides delta/CI. - Then user can toggle baseline matching mode between "All non-deposit" and "Same offers in periods without deposits"; values update accordingly. - Breakdown by instructor and offer is available; totals reconcile to component groups within 0.1pp rounding. - Export includes: dimension columns, deposit_noshow_rate, baseline_noshow_rate, delta_pp, ci_lower, ci_upper, n_deposit, n_baseline, match_mode.
Attribution: Revenue from Deposits
- Rule: Deposit-attributed revenue (net) = sum of payment amounts for bookings initiated via deposit, minus refunds and chargebacks applied to those payments, using payment posted date within selected timeframe and filters; also display gross (pre-refund/chargeback). - Then dashboard shows gross, net, refunds_total, chargebacks_total, and a stacked trend by time grain; amounts are in the account’s currency with clear currency code. - Then a toggle switches date basis between Payment Date and Booking Created Date; values update and a label shows the active basis. - Filters and breakdowns by offer, instructor, cohort, deposit type, and deadline rules are available; totals equal the sum of breakdown rows to the cent. - Export includes: date_grain, offer_id, instructor_id, cohort_id, revenue_gross, refunds, chargebacks, revenue_net, date_basis.
Refund and Chargeback Rates for Deposit Transactions
- Rule: Deposit refund rate (%) = refunded deposit transactions ÷ deposit transactions; Deposit chargeback rate (%) = chargebacked deposit transactions ÷ deposit transactions; time basis = transaction posted date within selected timeframe. - Then widgets display counts and percents for refunds and chargebacks separately, plus processor breakdown (e.g., Stripe) when available. - Then an alert badge appears if chargeback rate exceeds a configurable threshold (default 0.8%) for any breakdown row. - Filters for offer, instructor, cohort, deposit type, and deadline rules are supported; breakdown by offer and time grain is available. - Export includes: date_grain, offer_id, deposit_txn_count, refund_txn_count, refund_rate_percent, chargeback_txn_count, chargeback_rate_percent, processor.
A/B Testing: Deposit Tiers and Deadline Rules
- Rule: Experiments support variant definitions for tiered/percentage deposit amounts and deadline rules with random assignment and traffic split per offer/cohort. - Then the analytics UI displays variant-level metrics at minimum for: deposit uptake, hold-to-pay conversion, revenue_net, no-show rate, and refund/chargeback rates, each with 95% CI, p-values, and current sample sizes. - Then a "Winner" flag appears for a variant only when the primary metric (configurable, default = revenue_net) achieves p < 0.05, power ≥ 0.80, and min sample size per variant ≥ 100 bookings; otherwise shows "No clear winner". - Then guardrail alerts highlight if a candidate winner increases no-show rate or chargeback rate by ≥ 1.0pp with p < 0.10. - Export includes per-variant results with: metric_name, variant, n, point_estimate, ci_lower, ci_upper, p_value, power, winner_flag, guardrail_flags.

PlanFit Wizard

An interactive builder that recommends optimal pay‑in‑2/3/4 schedules based on start date, program length, and client payday preferences. Shows transparent due dates and studio cash‑flow projections to boost conversions and cut support questions for higher‑ticket classes and cohorts.

Requirements

Adaptive Installment Recommendation Engine
"As a studio owner, I want the wizard to automatically suggest the best pay‑in‑2/3/4 schedules aligned to my clients’ paydays so that more clients can enroll without manual calculations or support tickets."
Description

Core algorithm that generates optimal pay‑in‑2/3/4 schedules using inputs such as tuition, deposit, class start date, program length, and the client’s payday cadence. Applies constraints (minimum days between payments, last installment before program end with configurable buffer, weekend/holiday shifting, and timezone awareness), rounds amounts accurately, and scores plans for conversion vs. cash‑flow impact. Produces up to three recommended options with clear labels (Best Alignment, Fastest Payoff, Lowest Monthly). Integrates with pricing/discounts, taxes/fees, and checkout APIs. Includes deterministic test coverage and guardrails for edge cases (short programs, late enrollments, partial scholarships).

Acceptance Criteria
Generate labeled recommendations for pay‑in‑2/3/4
Given valid tuition, deposit, class_start_date, program_length_days, client_payday_cadence, applicable taxes/fees/discounts, and payer_timezone When the engine generates recommendations Then it returns between 1 and 3 unique plan options limited to 2, 3, or 4 installments And each option includes installment_count, labeled_recommendation, schedule (array of due_date and amount_cents), deposit_cents, and totals And labels include at most one of each: "Best Alignment", "Fastest Payoff", "Lowest Monthly" And no two options have identical schedule due_dates and amounts And options are ordered by overall recommendation score descending with tie‑breakers: fewer installments, then earlier final due_date
Respect minimum spacing and program‑end buffer constraints
Given config min_days_between_payments = N and program_end_buffer_days = B When computing installment due_dates for any option Then the difference between consecutive due_dates is >= N calendar days in the payer_timezone And the final installment due_date is <= (program_end_date − B days) And any candidate plan that cannot satisfy these constraints is excluded with reason_code "constraint_violation"
Weekend/holiday shifting with timezone awareness
Given a computed due_date that falls on a weekend or configured holiday in the payer_timezone When finalizing the schedule Then the due_date is shifted to the next business day in the payer_timezone And shifts do not violate min_days_between_payments or the program_end_buffer; if they would, the plan is excluded with reason_code "date_shift_violation" And due_dates are expressed as local calendar dates in the payer_timezone; DST transitions do not change the local calendar date
Accurate amounts, rounding, and totals with taxes/discounts/fees
Given currency with 2 fractional digits and grand_total_cents = (tuition − discounts + taxes + fees) When allocating installment amounts after deposit_cents is applied Then sum(deposit_cents + all installment amount_cents) equals grand_total_cents And each installment amount is rounded half‑up to the nearest cent And any rounding remainder is applied to the final installment and is within ±1 cent And taxes, discounts, and fees are allocated proportionally to installments and each component sums exactly to its total And no installment amount is negative and deposit_cents <= grand_total_cents
Payday alignment scoring and label assignment
Given client_payday_cadence (weekly, biweekly, semimonthly, or monthly) with required anchor details When scoring each feasible option Then an alignment_score is computed based on proximity of due_dates to paydays (within a ±2‑day window weighted by distance) And the option with highest alignment_score is labeled "Best Alignment" (ties: earlier final due_date, then fewer installments) And the option with earliest final due_date is labeled "Fastest Payoff" (ties: higher alignment_score) And the option with the lowest maximum installment amount is labeled "Lowest Monthly" (ties: higher alignment_score)
Edge handling for short programs, late enrollments, and scholarships
Given program_length_days insufficient for a 4‑installment plan under constraints When evaluating pay‑in‑4 Then it is excluded with reason_code "insufficient_time_window" Given checkout_date is within N days of class_start_date or after class_start_date When scheduling the first installment after deposit Then first installment due_date is >= (checkout_date + N days) in payer_timezone Given scholarships/discounts reduce grand_total_cents such that per‑installment falls below min_installment_amount_cents When generating options Then reduce installment_count until each installment >= min_installment_amount_cents or exclude with reason_code "below_min_installment" And if no multi‑pay plan is feasible, return an empty recommendation set with reason_code "no_viable_plan"
API output, cash‑flow projection, and determinism
Given successful generation of recommendations When returning the response Then each option includes: label, installment_count, schedule[{due_date, amount_cents, shifted:boolean}], deposit_cents, totals{subtotal_cents, discount_cents, tax_cents, fee_cents, grand_total_cents}, and scores{alignment, payoff_speed, cash_flow} And a cash_flow_projection aggregates inflow_cents per calendar date including deposit at checkout_date And the response validates against JSON Schema InstallmentRecommendationsV1 Given identical inputs When the engine runs multiple times Then outputs are identical and deterministic And unit/integration tests cover >= 90% of algorithm branches and all specified edge cases pass
Client Payday Preference Capture
"As a client, I want to quickly indicate when I get paid so that my installment due dates line up with my cash flow."
Description

Interactive step in the wizard that collects the client’s payday pattern (weekly day-of-week, biweekly next payday date, semimonthly days like 1st/15th, or monthly specific day) with a quick picker and helper text. Validates feasibility against the class start date and program length, auto-calculates the next payday, and stores preferences on the client profile for reuse. Provides sensible defaults and a fallback when payday is unknown. Includes consent text for using the preference to schedule automatic charges.

Acceptance Criteria
Payday Pattern Quick Picker and Helper Text
Given the Client Payday Preference step is displayed, When the user focuses the payday pattern selector, Then a quick picker shows four options: Weekly, Biweekly, Semimonthly (1st & 15th), and Monthly, each with concise helper text describing how due dates are aligned. Given a user navigates via keyboard or screen reader, When interacting with the quick picker and its sub-controls, Then all controls are reachable via Tab/Arrow keys, have accessible names/roles/states, and meet WCAG 2.1 AA label and contrast requirements. Given the device width is ≤ 375px, When the quick picker renders, Then it fits without horizontal scroll and touch targets are at least 44x44 px. Given an option is selected, When the user switches between patterns, Then context-specific inputs (weekday chips, date picker, day-of-month selector) appear instantly without page reload and the helper text updates accordingly.
Weekly Payday Selection and Next Payday Calculation
Given I select Weekly and choose a weekday (e.g., Friday), When I confirm the selection, Then the system calculates and displays the next payday as the next occurrence of that weekday on or after today in my profile timezone (or the studio default if none). Given today matches the selected weekday, When I open the step, Then the next payday defaults to today in the applicable timezone. Given I change the selected weekday, When I do so, Then the displayed next payday updates immediately and remains within a valid calendar date (no time-of-day dependence). Given the timezone changes on my profile during the session, When recalculating, Then the next payday re-renders using the new timezone.
Biweekly Payday Anchor and Next Payday Derivation
Given I select Biweekly, When I enter or pick my next payday date, Then the system validates the date is today or a future date in the applicable timezone and shows an inline error if invalid (past/format missing). Given a valid next payday date (anchor) is provided, When I proceed, Then the system stores the anchor and displays the subsequent payday as anchor + 14 days. Given I edit the anchor date, When I change it, Then the subsequent biweekly date recalculates instantly and the validation state updates accordingly. Given the anchor is the 29th Feb on a leap year, When computing +14 days, Then the result uses standard calendar addition and remains valid.
Semimonthly and Monthly Payday Calculation and Edge Cases
Given I select Semimonthly (1st & 15th), When I proceed, Then the next payday is the next occurrence of either the 1st or 15th on or after today in the applicable timezone and is displayed to the user. Given I select Monthly and choose a day N (1–31), When N exceeds the number of days in a given month, Then the system uses that month’s last calendar day for that installment and indicates the adjustment (e.g., “moved to last day of month”). Given I select Monthly with a valid day N, When I confirm, Then the next payday is the next occurrence of day N on or after today in the applicable timezone. Given February or a 30-day month follows and N=31, When calculating the next payday, Then the system sets the payday to Feb 28/29 or the 30th respectively and shows the adjusted date.
Fallback When Payday Is Unknown
Given I select “I’m not sure” or skip payday selection, When I proceed, Then the wizard defaults to an equal-interval schedule anchored to the class start date and displays a note that installment dates will not align to a payday. Given fallback is used, When I submit the step, Then the system stores the payday preference as unknown on my profile with a flag allowing later update without creating a duplicate profile. Given fallback is selected, When the wizard calculates recommendations, Then feasibility checks run without requiring payday data and the flow is not blocked due to missing payday alignment.
Persisting Preference and Consent on Client Profile
Given I provide a payday preference, When I continue from the step, Then the system persists: pattern type, parameters (weekday/date/day-of-month), derived next payday date, timezone used, capture timestamp, and a consent flag with consent text version ID on my client profile. Given a saved preference exists, When I revisit the wizard, Then the step preselects the saved pattern and parameters and allows editing before proceeding. Given consent is required to schedule automatic charges, When I attempt to continue without giving consent, Then an inline error appears and the Next/Continue action is disabled until consent is provided. Given I update my payday preference later, When I save, Then an audit trail entry records prior and new values with timestamp and user context.
Feasibility Validation Against Class Start and Program Length
Given a payday pattern is selected, When the wizard generates pay‑in‑2/3/4 installment schedules, Then no due date occurs before the class start date or after the program end date derived from program length. Given the selected payday pattern yields insufficient feasible dates for the chosen installment count, When this occurs, Then the system displays a clear inline message explaining the conflict and suggests the nearest feasible option(s), disabling Next until a feasible option is selected. Given a feasible schedule is available, When shown, Then each due date is labeled with its payday alignment (e.g., “Aligned to 1st/15th” or weekday) and the user can proceed without error.
Transparent Installment Timeline UI
"As a client, I want a clear breakdown of each installment’s date and amount so that I can decide confidently at checkout."
Description

Checkout and preview UI that clearly displays deposit due now, each installment’s date and amount, applied discounts, taxes/fees, and the selected payment method. Shows timezone, late fee policy, retry schedule, and cancellation/refund terms via tooltips and inline disclosures. Supports responsive layouts, accessibility (WCAG 2.1 AA), print/shareable links, and localized date/number formats. Updates in real time when plan options or payday preferences change.

Acceptance Criteria
Display of Installment Breakdown at Checkout
Given a shopper selects an installment plan at checkout When the checkout summary renders Then the UI displays: Deposit Due Now amount, each installment’s date and amount, itemized discounts, itemized taxes/fees, selected payment method (masked), order subtotal, grand total, and "Total Due Today" And the sum of deposit + future installments + taxes/fees − discounts equals the grand total within 0.01 of the currency unit And amounts are shown with the correct currency symbol and precision for the selected locale And if no discounts apply, the discounts row is hidden without leaving a visual gap And if taxes/fees are zero, the row label remains visible with a value of 0 in the correct format
Real-Time Update on Plan or Payday Preference Change
Given the shopper changes between pay‑in‑2/3/4 or adjusts payday preference When the selection changes Then installment dates and amounts recompute and update in the UI within 500ms without a full page reload And the "Total Due Today" and grand total recalculate accordingly And any applied discount proration reflects the new schedule And the cash‑flow projection badge (if shown) updates to match the new timeline And an inline loading indicator appears if recomputation exceeds 300ms and clears when complete
Policy Tooltips and Inline Disclosures
Given the checkout timeline shows Late Fee, Retry Schedule, and Cancellation/Refund info icons When the user focuses (keyboard), hovers, or clicks an icon Then a tooltip opens with the policy title, a concise description (max 240 chars), and a "Learn more" link to full policy And tooltips are dismissible via Escape, clicking outside, or moving focus away And tooltips are positioned without obscuring key amounts; if overlap would occur, they auto-reposition And inline disclosure text is visible without interaction for legally required statements And all policy text matches the latest policy version from configuration
Timezone and Localization Display
Given the shopper’s timezone is detected from profile or browser When the installment timeline is shown Then the timezone label (e.g., "All dates in America/Los_Angeles (UTC−07:00)") appears above or beside the schedule And all dates format according to the shopper’s locale (e.g., en-US: MMM D, YYYY; fr-FR: D MMM YYYY) And numbers and currency use locale-appropriate separators and symbols And when the user changes timezone/locale via settings Then all displayed dates and numbers re-render within 500ms, with no date shifting across days due to DST handling And DST transitions do not change the displayed installment calendar dates
Responsive Layout Across Devices
Given common viewport widths (320–480, 481–768, 769–1024, >1024 px) When the timeline UI is rendered Then no horizontal scrolling is required to view primary content And tap targets are at least 44x44 px on touch devices And font sizes are at least 16px base on mobile with readable line height And the payment summary and schedule stack vertically on mobile and align side-by-side on desktop without overlapping And images/icons are crisp on 2x/3x pixel density screens
Accessibility WCAG 2.1 AA Compliance
Given a keyboard-only user navigates the checkout When tabbing through the timeline and policy elements Then all interactive elements are reachable and operable (WCAG 2.1: 2.1.1, 2.1.2) And focus order is logical and visible (2.4.3, 2.4.7) And tooltips open on focus and are announced with accessible names/roles (1.3.1, 4.1.2) And color contrast meets 4.5:1 for text and 3:1 for large text/icons (1.4.3, 1.4.11) And amounts and dates are exposed to screen readers with programmatic associations to their labels (1.3.1) And errors (e.g., invalid payment method) are announced via ARIA live regions (3.3.1, 3.3.3)
Print-Friendly and Shareable Timeline
Given the shopper selects "Print" or "Save/Share Timeline" When printing Then a print stylesheet renders all schedule rows expanded, hides non-essential UI, and fits on <=2 pages for typical 3–4 installment plans with 12mm margins And contrasts remain readable in grayscale When generating a shareable link Then a unique, non-guessable tokenized URL is created that opens a read-only timeline view with masked payment method details And the shared view reproduces the same dates, amounts, discounts, taxes/fees, timezone, and locale as the source within 0.01 currency units And the link returns HTTP 200 when valid and 404 when expired or revoked
Studio Cash‑Flow Projection Dashboard
"As a studio owner, I want to see how each plan impacts my monthly cash flow so that I can choose the option that balances conversion and liquidity."
Description

Owner-facing view that projects expected cash inflows over time per class/cohort based on selected plan mixes (pay‑in‑full vs pay‑in‑2/3/4). Provides charts and tables for monthly projections, cumulative revenue curves, and best/worst-case ranges with configurable failure/retry assumptions. Includes scenario comparison, filters by class and plan, CSV export, and performance caching. Integrates with refunds, cancellations, and late enrollment adjustments to keep projections accurate.

Acceptance Criteria
Monthly Projection by Class and Plan Mix
Given a studio timezone of America/New_York and a date range of 2025-10-01 to 2025-12-31 And a cohort "Yoga 101" with 10 enrollments: 4 pay-in-full $400 due 2025-10-01; 3 pay-in-2 $220 due 2025-10-01 and 2025-11-01; 3 pay-in-4 $120 due 2025-10-01, 2025-11-01, 2025-12-01, 2026-01-01 When the dashboard loads with plan mix = actual enrollments and failure assumptions = 0% Then the monthly projection table shows totals: 2025-10 = $2620; 2025-11 = $1020; 2025-12 = $360 And the monthly bar chart values exactly match the table totals within ±$0.01 And the sum of displayed months equals $4000 And installments outside the selected date range (e.g., 2026-01-01) are excluded from both chart and table
Cumulative Revenue Curve Toggle
Given the dataset in "Monthly Projection by Class and Plan Mix" and the view toggle set to Cumulative When viewing October through December 2025 Then the cumulative curve is non-decreasing and ends at $4000 on 2025-12-31 (excluding 2026 installments) And hovering on 2025-11 shows a cumulative value of $3640 And currency formatting uses the studio default currency and locale
Best/Worst-Case with Failure/Retry Assumptions
Given a month with 100 scheduled installments of $100 each (base $10,000) And failure rate = 5%, max retries = 2, per-retry success probability = 50% When the assumptions are applied Then best-case monthly projection = $10,000 And worst-case monthly projection = $9,500 And likely-case monthly projection = $9,875 computed as base * (1 - failure_rate * (1 - (1 - retry_success_prob)^max_retries)) And the chart displays a shaded range from worst to best and a line for likely-case And changing any assumption updates values within 1 second of input
Scenario Comparison and Delta Highlighting
Given Scenario A (50% pay-in-full, 50% pay-in-4) and Scenario B (30% pay-in-full, 70% pay-in-2) for the same cohort and date range When the user enables Compare and selects A vs B Then both scenarios render side-by-side monthly tables and cumulative curves with distinct legend labels And per-month and total deltas (A - B) are displayed with positive values in green and negatives in red And up to 3 scenarios can be compared concurrently without legend overlap And the Compare view can be exported to CSV with a column indicating Scenario Name and Delta columns
Adjustments for Refunds, Cancellations, and Late Enrollments
Given an initial projection for a cohort with 20 installments in November totaling $5,000 And a partial refund of $200 issued on 2025-11-10 and a cancellation effective 2025-11-15 removing a $250 installment due 2025-12-01 And a late enrollment on 2025-11-20 adding two installments: $150 on 2025-11-20 and $150 on 2025-12-20 When the dashboard refreshes Then the November projection becomes $4,950 And the December projection is adjusted by -$100 relative to the prior value And the cumulative curve reflects these adjustments within 2 minutes of the underlying events being saved And negative adjustments are rendered with a distinct style and included in totals
Filtering and CSV Export Integrity
Given filters Class = "Yoga 101", Plan Types = [Pay-in-2, Pay-in-4], Date Range = 2025-10-01 to 2025-12-31 When filters are applied Then projections exclude pay-in-full installments and only include "Yoga 101" within the date range And the total displayed equals the sum of visible table rows within ±$0.01 And clicking Export CSV downloads a file whose totals and row counts match the on-screen filtered data and includes a header with Class, Plan Types, Date Range, and Studio Timezone And all monetary values are rounded to 2 decimal places and use the studio currency
Performance Caching and Freshness Indicators
Given a studio with more than 10,000 installments across 100 classes When the dashboard loads cold Then initial render completes within 3 seconds at p95 and 1.5 seconds at p50 using cached aggregates And a "Last updated" timestamp shows the cache build time in the studio timezone And when new enrollment/payment data is added, the dashboard auto-refreshes within 60 seconds or upon manual Refresh, and the timestamp updates And pressing Refresh bypasses cache and rebuilds aggregates, completing within 10 seconds at p95
Auto Enroll Waitlist‑to‑Installment Conversion
"As a studio owner, I want waitlisted clients to seamlessly convert into an installment plan when a spot opens so that seats are filled quickly without manual admin."
Description

Flow that, when a spot opens, automatically offers the waitlisted client their selected installment plan, reserves the seat, collects the deposit immediately, and schedules remaining payments. Supports timed holds, fallback to next waitlister on expiry, and dunning if the initial payment fails. Integrates with existing waitlist logic, inventory/double‑booking prevention, and audit logging to ensure traceability.

Acceptance Criteria
Auto Offer + Timed Seat Hold + Deposit Success
Given a class has at least 1 open seat and the top-priority waitlisted client has a selected pay-in plan When the seat becomes available Then the system sends the client an offer containing deposit amount, installment due dates, and a hold expiry timestamp And the system reserves exactly 1 seat exclusively for the client for the configured hold window (e.g., 2 hours) And other clients cannot book that seat during the hold window And if the client accepts within the hold window and the deposit payment succeeds, the client is enrolled immediately And the remaining installments are scheduled according to the selected plan And class capacity and roster reflect the enrollment instantly
Hold Expiry → Auto-Fallback to Next Waitlister
Given an offer with a seat hold is active for a waitlisted client When the client does not complete payment before the hold expiry or explicitly declines Then the offer is invalidated and the seat is released from the client And the offer and seat hold are immediately re-issued to the next eligible waitlisted client per existing waitlist priority rules And the process repeats until a client pays the deposit or the waitlist is exhausted And if the waitlist is exhausted, the seat returns to general inventory for public booking And an audit entry records the expiry and reassignment events
Initial Deposit Failure → Dunning and Outcome
Given the client accepts the offer but the initial deposit payment attempt is declined by the processor When the decline is received Then the client is notified with the failure reason (generic, non-PCI) and prompted to update payment method And the system initiates dunning with up to 3 retries at 15 minutes, 2 hours, and 24 hours (configurable) And the seat hold persists during the dunning window up to a maximum of 24 hours (configurable) And if any retry succeeds within the window, the client is enrolled and installments are scheduled And if all retries fail or the dunning window elapses, the offer is cancelled, the seat is released, and the next waitlisted client is offered And all attempts and outcomes are logged with timestamps and payment intent IDs
Atomic Reservation and Double-Booking Prevention
Given concurrent events (multiple seats opening, multiple clients accepting, or admin capacity edits) When the system processes seat reservation and payment authorization for an offer Then reservation and payment authorization are executed atomically to prevent oversubscription And at no time can more than available seats be reserved or enrolled And if the client already has a conflicting booking at the same time, acceptance is blocked with a clear error and no seat is reserved And if payment authorization succeeds but schedule creation fails (or vice versa), a compensating rollback restores inventory and cancels the offer And inventory counts, roster, and financial records remain consistent after any failure or rollback And all conflict rejections and rollbacks are captured in the audit log
Plan Integrity: Deposit and Installment Schedule Accuracy
Given the client previously selected a pay-in 2/3/4 plan from PlanFit Wizard When the offer is generated Then the deposit amount, number of installments, installment amounts, and due dates match the Wizard’s calculation based on class start date, program length, client timezone, and payday preference And due dates respect configured billing cutoffs and timezone rules, with timestamps shown to the client in their local time And the installment schedule avoids past dates and does not schedule any payment after the program end date unless allowed by configuration And the client-facing offer and confirmation display the exact schedule and totals before payment And the studio dashboard shows the same schedule and projected cash-flow for the enrollment
Audit Logging and Event Notifications
Given any state change in the waitlist-to-installment flow (offer sent, hold started/expired, payment success/failure, fallback, enrollment, cancellation) When the event occurs Then an immutable audit record is created with timestamp (UTC), actor/system, client ID, class/cohort ID, seat/offer IDs, plan ID, and payment intent/transaction IDs And PII/PCI data (full card numbers, CVV) are never stored in logs And client notifications include offer details, hold expiry, and next steps; studio notifications include enrollment status and payment outcomes And notifications are delivered via the configured channels (email, SMS, in-app) with retry on transient failures And the full event timeline is visible in the admin UI and exportable via API
Payment Method Vaulting and AutoPay Consent
"As a client, I want to securely save a payment method and authorize automatic charges so that I don’t miss payments or lose my spot."
Description

Securely captures and tokens card/ACH payment methods with explicit authorization for automatic installment charges. Supports 3DS/SCA where applicable, ACH micro‑deposit verification, pre‑authorization for deposit, card‑expiry monitoring with proactive update prompts, and mid‑plan method updates. Ensures PCI compliance, stores consent records, sends mandated notices, and issues receipts for each charge. Provides fallback method capture on failure without losing seat reservation windows.

Acceptance Criteria
Card Vaulting with SCA and Explicit AutoPay Consent
Given a user in the PlanFit Wizard selects Card and enables AutoPay When the user submits card details and authorization Then the card is tokenized without storing PAN, creating a customer payment method ID linked to the plan And if 3DS/SCA is required, a challenge is presented; success finalizes tokenization; failure leaves the plan in Payment Method Pending without charging and without losing the seat And a consent record is stored with user ID, plan ID, last4, brand, IP, timestamp, authorization text hash, and locale And a confirmation message (email/SMS) is sent with consent summary within 1 minute of completion
ACH Micro‑Deposit Verification and AutoPay Authorization
Given a user selects ACH and enables AutoPay during plan setup When the user connects a bank account via instant auth or manual entry Then a bank account token is created; if instant auth succeeds, the account is verified immediately; otherwise two micro‑deposits are initiated And the user must confirm the two deposit amounts within 5 business days with a maximum of 3 attempts And until verification succeeds, the plan shows Verification Pending, AutoPay is disabled, and the seat is provisionally reserved for 72 hours And upon successful verification, a consent record is stored and AutoPay is activated; a confirmation message is sent within 1 minute
Pre‑Authorization for Deposit and First Installment
Given a plan requires a deposit or first installment at signup and Card is selected When the user submits the payment method and authorization Then a pre‑authorization for the due amount is attempted and the result is displayed within 5 seconds And on success, the authorization is captured immediately or on the configured date; on failure, no capture occurs and the plan remains Incomplete And after 3 failed attempts, the user is prompted to add an alternate method; the seat is held for 15 minutes with a visible countdown And an audit log records auth ID, amount, status, timestamp, and method ID
Mid‑Plan Payment Method Update and Consent Refresh
Given a plan is Active with future installments scheduled When the user updates the payment method from their portal Then the new method is tokenized and set as primary; the previous method remains as fallback unless explicitly removed And updated consent text is displayed and must be accepted; a new consent record is stored with timestamp and authorization text hash And scheduled payments switch to the new method within 1 minute with no duplicate charges And if an installment is past due or due within 24 hours, the system retries immediately with the new method
Card Expiry Monitoring and Proactive Update Prompts
Given a vaulted card will expire before the next scheduled installment When the card is within 30, 14, and 3 days of expiry Then reminder messages are sent at those intervals with a secure update link And if the card is updated successfully, all future reminders for that card stop And if the card expires before update, Account Updater is attempted where available; otherwise failure workflow and fallback capture are triggered And admin sees an Expiring Card status badge on the plan and customer record
Mandated Notices, Receipts, and Consent Recordkeeping
Given an installment is scheduled or executed (success or failure) When notifications are required by network or ACH rules Then a pre‑notification is sent at least 3 days prior for ACH, detailing amount, date, method, and cancellation instructions And upon success, a receipt is sent within 1 minute; upon failure, a failure notice with a secure dunning link is sent within 1 minute And an immutable audit entry is stored per event with charge ID, amount, plan ID, customer ID, method ID, outcome, network response codes, consent record ID, and notification delivery status And records are retained for at least 24 months and are exportable by admins
Fallback Method Capture After Charge Failure with Seat Protection
Given a scheduled installment fails for insufficient funds, expired card, SCA failure, or network error When the failure is recorded Then the seat remains reserved for a configurable grace period (default 72 hours) with a visible countdown to the user and admin And the system follows a dunning schedule (e.g., retry at T+1 and T+3 days) and sends a secure link to add a fallback method And if a fallback method is added, consent is captured and the charge is retried immediately; on success, the plan status returns to Active and the grace period ends And if the grace period expires without successful payment, the seat is released and the plan is canceled per policy; all actions are logged
Installment Reminders and Dunning Automation
"As a studio owner, I want automated reminders and smart retries for installments so that late payments decrease without adding manual work."
Description

Automated pre‑due reminders (email/SMS/push), same‑day alerts, and structured retry schedules aligned to the client’s payday cadence. Includes editable templates, localization, quiet hours, and opt‑in/opt‑out controls to meet compliance (e.g., TCPA/GDPR). Emits webhooks/events for downstream systems, tracks delivery/engagement metrics, and summarizes recovery performance in reports.

Acceptance Criteria
Pre‑Due Reminder Cadence (Email/SMS/Push)
Given a client has an upcoming installment due on D and has opted in to email and SMS When local time is D−7 at 09:00 Then send exactly one email and one SMS containing program name, installment n/N, amount due, due date/time D, and a payment link And log each send with message_id, channel, locale, template_id, and provider status within 5 minutes When local time is D−1 at 09:00 and the invoice is still unpaid Then send exactly one additional reminder via each opted-in channel And all scheduled sends respect configured quiet hours (default 21:00–08:00 local) by deferring to 08:00 next permitted window
Same‑Day Due Alerts Without Duplicates
Given an installment remains unpaid at D 09:00 local and the client is opted in When the due‑today window opens at 09:00 Then send one due‑today alert via each opted‑in channel When it remains unpaid by D 17:00 local Then send one follow‑up alert via the primary channel only And do not send more than two alerts on D and never within 15 minutes of each other And if any alert falls within configured quiet hours (default 21:00–08:00 local), defer to 08:00 next permitted window unless payment has been recorded And no alerts are sent after payment is confirmed
Dunning Retry Aligned to Client Payday
Given a payment attempt fails on date F and the client’s payday cadence is biweekly Friday When retries are scheduled Then create up to three retries on the next three payday dates at 09:00 local (P1, P2, P3) And if a payday retry fails, schedule one contingency retry 2 days after each payday at 09:00 And enforce minimum spacing of 24 hours between retries And cancel all future retries immediately upon successful capture And defer any retry that falls within configured quiet hours to 08:00 next permitted window And record each attempt’s timestamp, outcome, reason_code, and gateway response; on final failure, mark the installment delinquent and notify the admin
Consent, Opt‑In/Opt‑Out, and Compliance Logging
Rule: SMS and push are only sent with explicit opt‑in that includes consent timestamp, source, and IP; email follows locale‑appropriate legal basis with documented record Rule: Every outbound SMS includes opt‑out instructions; when the user replies STOP, opt‑out is enforced within 60 seconds and a confirmation SMS is sent Rule: Consent and preference changes are written to an immutable audit log with actor, timestamp, and previous/new values Rule: Upon GDPR erase request, personal data used for messaging is deleted or pseudonymized and messaging disabled within 30 days, while non‑PII event metadata is retained for reporting compliance
Template Editing and Localization
Given an admin with permissions edits a reminder template When saving changes Then required placeholders {client_name},{due_date},{amount},{payment_link} are validated and save is blocked if any are missing And a preview renders with sample data and shows SMS character count with concatenation warning if >160 chars When a client locale L is set Then messages render using template L with locale‑correct date/time/currency; if L is unavailable, fallback to en‑US And all sends record template_id and template_version for traceability
Webhooks and Event Emission for Downstream Systems
Given registered webhook endpoints When events occur (reminder_queued, reminder_sent, delivery_status_changed, opened, clicked, bounced, payment_succeeded, payment_failed, retry_scheduled, retry_executed) Then emit an event within 30 seconds with payload: event_type, entity_ids, ISO‑8601 timestamp, and HMAC‑SHA256 signature And deliveries use at‑least‑once semantics with exponential backoff (5 attempts over ~15 minutes) and include an idempotency key And endpoints with 10 consecutive failures are auto‑disabled and an admin notification is sent
Delivery/Engagement Metrics and Recovery Report
Given the reporting dashboard and a selectable date range When metrics load Then display sends, delivery rate, open rate, click‑through rate, payment recovery rate, recovered revenue, average days to recovery, and breakdown by channel and cadence And metrics refresh within 15 minutes of new events And filters by program, instructor, and cohort are available and affect all widgets consistently And CSV export includes row‑level events with message_id, client_id, channel, timestamps, and outcomes And recovery rate is defined as the percentage of failed installments subsequently paid within 30 days of first dunning attempt, and the definition is shown in‑app

Fallback Pay

Collect a backup payment method at checkout (card, ACH, or wallet) and auto‑switch on soft declines. Clients get a heads‑up and one‑tap confirmation while rosters stay intact—recovering revenue without staff intervention.

Requirements

Backup Payment Capture at Checkout
"As a client booking a class, I want to add a backup payment method during checkout so that my reservation is not lost if my primary payment fails."
Description

Collect a secondary payment method during checkout in addition to the primary method, supporting card, ACH, and mobile wallets. Present clear consent copy that explains when the backup may be used, with links to terms and studio policy. Allow customers to select and reorder backup preference, prefill from saved methods, and add new methods with real-time validation. Tokenize and securely vault all methods via the existing payment gateway, associate them to the customer profile, and make them available across web and mobile checkout. Display non-sensitive identifiers (last4, bank name, wallet type) and provide an opt-out. Ensure the flow adds minimal friction: one additional step with progressive disclosure, localized copy, and accessibility compliance. Persist the backup selection on the booking and order for later orchestration.

Acceptance Criteria
Add Backup Payment Method with Real-Time Validation
Given the customer is on the checkout payment step with a primary payment method selected When they choose to add a backup payment method Then they can select one of: Card, ACH, or Mobile Wallet And for Card: number passes Luhn, expiry is in the future, CVV matches brand length, and invalid inputs block submission with inline errors And for ACH: routing number checksum validates, account number length is within allowed range, account type is selectable, and invalid inputs block submission with inline errors And for Mobile Wallet: wallet option is shown only when supported by device/browser, and selection triggers the native wallet sheet and returns a token on success And the backup method cannot be saved if client-side or gateway validation fails And submission latency to tokenization response P95 <= 2.0s
Explicit Consent and Policy Links for Backup Usage
Given the backup payment step is displayed When the customer reviews the consent copy Then the copy clearly states that the backup may be charged if the primary payment soft-declines and references applicable terms and studio policy And there are two working links: Terms of Service and Studio Payment Policy, each opening in a new tab And the customer must check an unchecked consent box before saving a backup method And upon consent, the system stores consent timestamp, consent text version ID, and customer IP on the order and customer profile And if consent is not provided, no backup payment method is saved
Select, Prefill, and Reorder Backup Preferences
Given the customer has one or more saved payment methods When the backup step loads Then saved methods are listed with non-sensitive identifiers and are selectable as backup And the customer can add a new method or select up to three saved methods as backup candidates And the customer can reorder backup priority via drag-and-drop and via keyboard controls And the chosen order is displayed and validated before confirmation And changes persist when the customer navigates back and forth within checkout
Tokenization, Vaulting, and Cross-Channel Availability
Given a backup payment method is added or selected When the customer confirms the backup selection Then all payment data is tokenized via the existing payment gateway and no raw PAN or bank account numbers are stored or logged And tokens are associated with the customer profile and the current order/booking And the tokens and preferences are available for subsequent checkouts on web and mobile via authenticated API calls And gateway and application logs contain no sensitive data and show success/failure codes And on tokenization failure, the customer sees an inline error and can retry without losing entered data
Non-Sensitive Identifiers and Opt-Out Control
Given the backup payment step is displayed When payment methods are listed Then cards show brand, last4, and expiry month/year; ACH shows bank name (if available) and account last4; wallets show wallet type (e.g., Apple Pay, Google Pay) And no full account numbers, full bank names for ACH if not provided by gateway, or CVVs are displayed And an explicit Skip adding a backup method option is visible and accessible And selecting Skip records an opt-out flag on the order and customer session and allows checkout to proceed without error
Low-Friction, Localized, and Accessible Flow
Given the checkout baseline has no backup step When the backup capability is enabled Then the checkout adds no more than one additional step/screen And using a saved method requires no more than two clicks/taps to confirm And advanced fields for adding a new method are hidden until the user selects Add a new backup method And copy is localized to the customer’s selected locale and dynamic field formats (e.g., card expiry, routing/account labels) match locale conventions And the step meets WCAG 2.1 AA: keyboard navigable, focus-visible, proper ARIA labels, labels associated to inputs, and color contrast >= 4.5:1 And screen reader announces validation errors and consent status changes And the flow is fully functional on desktop and mobile viewports >= 320px width
Persist Backup Selection on Booking and Order Records
Given the customer completes checkout When the order and booking are created Then the backup payment tokens, ordered preference, consent metadata, and opt-out status (if applicable) are persisted on both the order and booking records And these fields are retrievable via internal API and admin UI And the data is available to downstream payment orchestration processes within 1 minute of order creation And updates to backup preference before order submission are reflected in the persisted records
Soft-Decline Detection and Auto-Switch Orchestration
"As a studio owner, I want the system to automatically switch to a backup method on soft declines so that bookings are preserved and I don’t lose revenue."
Description

Detect soft-decline responses from processors in real time, normalize error codes across providers, and route payment attempts through a configurable retry and fallback policy. On a soft decline, hold the reservation and roster spot, queue retries with exponential backoff where appropriate, and automatically switch to the backup method when policy conditions are met. Maintain idempotency keys and atomic updates to avoid duplicate charges, and write machine-readable state transitions (attempted, retried, switched, captured, failed) to the payments event stream. Expose a deterministic decision log per transaction for audit and support. Respect cutoffs relative to class start time to guarantee roster integrity.

Acceptance Criteria
One-Tap Client Confirmation Notifications
"As a client, I want a one-tap confirmation prompt before my backup method is charged so that I stay informed and in control of my payments."
Description

Send an immediate heads-up to the client when a soft decline occurs, including a one-tap confirm action to proceed with charging the backup method and a one-tap cancel to update the booking. Support push, SMS, and email with deep links to web and app, expiring tokens, and localization. If the policy requires explicit consent, block auto-capture until the client confirms or the timer elapses; otherwise, proceed automatically while still notifying the client. Provide real-time status in the client portal and allow method swap within the same flow. Record the client’s consent with timestamp, channel, and IP/device metadata.

Acceptance Criteria
Explicit Consent Required on Soft Decline
Given org setting explicit_consent_required = true and timeout_minutes = 15 and timeout_action = "cancel" And a booking with a valid backup payment method on file When the primary payment attempt results in a soft decline Then the system sends heads-up notifications via all enabled channels (push, SMS, email) within 5 seconds containing one-tap Confirm and one-tap Cancel deep links And auto-capture is blocked until the client acts or the timer elapses And if the client taps Confirm before timeout with a valid token, the backup method is charged successfully, the booking status changes to Paid, and the roster remains unchanged And if the client taps Cancel before timeout, no charge is attempted, the booking status changes to Canceled, the roster spot is released, and staff are notified And if the timer elapses without client action, no charge is attempted and timeout_action "cancel" is applied to the booking
Auto-Capture Allowed on Soft Decline
Given org setting explicit_consent_required = false and grace_window_minutes = 10 And a booking with a valid backup payment method on file When the primary payment attempt results in a soft decline Then the system initiates auto-capture on the backup method within 5 seconds while sending notifications via all enabled channels And if the auto-capture succeeds, the booking remains Confirmed, the roster remains unchanged, and the client portal shows "Paid via backup" with method details masked And if the client taps Cancel within the grace window and the transaction is still only authorized, the authorization is voided, the booking is canceled, and the roster spot is released And if capture has already completed when the client taps Cancel, a refund is created per refund_policy within 24 hours and the booking is canceled
Cross-Channel One-Tap Deep Links
Given push, SMS, and email channels are enabled for the client When a soft decline occurs Then each message includes a unique, signed, single-use deep link that opens the native app via universal/app links when installed or a responsive web fallback otherwise And the deep link carries booking context and presents Confirm and Cancel actions without requiring login And tokens expire 15 minutes after issuance; using an expired token shows an expiration message and offers to request a new link without performing any action And if a deep link is tampered with or used for a different booking or client, the action is rejected with 401 and no booking changes occur And successful Confirm/Cancel completes end-to-end in 2 seconds or less from tap to confirmation screen
Real-Time Status in Client Portal
Given the client is viewing the booking details in the portal When the primary payment soft-declines Then the portal reflects "Action needed" with a countdown timer and Confirm/Cancel/Swap Method controls within 3 seconds And when the client completes Confirm or Cancel from any channel, the portal status updates within 3 seconds to "Paid" or "Canceled" accordingly and the roster indicator updates only on cancel And an activity feed entry is added for Soft Decline, Notification Sent (per channel), Client Confirmed/Cancelled, and Payment Attempted/Result with timestamps
Payment Method Swap Within One-Tap Flow
Given the client opens the one-tap action screen from any channel When the client chooses Swap payment method Then the client can select an existing stored method (card/ACH/wallet) or securely add a new method via PCI-compliant collection And upon confirming, the system charges the selected method and records it as the backup for this booking; the original backup is not charged And if the charge succeeds, the booking becomes Paid and the roster remains unchanged; if it fails, the client returns to the action screen with a clear error and may retry or Cancel And if a charge has already completed (auto-capture) before the swap is submitted, the system disallows the swap and displays "Payment already completed"
Localization of Notifications and Action Views
Given the client’s preferred locale is set and the organization has matching templates When notifications and action views are generated Then all content (subject, body, buttons, error/success messages) is localized to the client’s locale with fallback to the org default if missing And dynamic values (dates, times, currency) are formatted per locale And right-to-left layouts (e.g., ar) render correctly without truncation or broken links And at a minimum, en-US, es-ES, and fr-FR variants are available and selectable in QA
Consent Recording and Metadata Audit Trail
Given the client taps Confirm or Cancel via any channel When the action is processed Then the system records an immutable consent event including booking_id, action (confirm/cancel), outcome (success/failure), timestamp (UTC), channel (push/SMS/email/web/app), client_id, IP address, device metadata (user agent, device model/OS if available), and token_id And the consent record is linked to the payment attempt and is retrievable in admin audit logs within 2 seconds of the action And no sensitive PAN or secret token values are stored; only references/hashes are persisted And audit records are retained for at least 24 months and are exportable in CSV and JSON
Merchant Policy Controls and Studio Configuration
"As a studio owner, I want to configure fallback policies, allowed methods, and messaging so that Fallback Pay aligns with my business rules and customer experience."
Description

Provide studio owners with a settings panel to enable Fallback Pay, choose allowed backup types (card, ACH, wallets), define retry counts and intervals, require or skip client confirmation, set message templates, and set class start cutoffs. Allow exceptions by product type (drop-in, package, membership), price threshold, and customer segment. Support test mode and sandbox transactions for validation. Surface a summary of the current policy with effective dates and versioning, and allow per-class overrides when publishing a schedule.

Acceptance Criteria
Enable Fallback Pay and Allowed Methods Settings
Given I am a studio owner with Admin role on Settings > Payments > Fallback Pay, When I toggle Fallback Pay On, select allowed backup methods (Card, ACH, Wallet), and click Save, Then the setting persists and displays as On with the selected methods after page reload. Given Fallback Pay is On with allowed methods {Card, Wallet}, When a client reaches checkout, Then the backup method selector only offers Card and Wallet. Given Fallback Pay is Off, When a client reaches checkout, Then no backup payment method is requested. Given I attempt to save Fallback Pay On with no allowed methods selected, When I click Save, Then an inline validation error states "Select at least one backup payment method" and the save is blocked. Given I change allowed methods and Save, Then the change is captured in an audit log with user, timestamp, and previous/new values.
Retry and Class Start Cutoff Configuration
Given I set Retry Count to 3 and Retry Interval to 2 hours and Class Start Cutoff to 30 minutes, When a soft decline occurs at 08:00 for a class starting at 10:00, Then retries are scheduled at 10:00 and later retries after 09:30 are suppressed due to cutoff. Given Retry Count is outside 1–6, When I click Save, Then validation blocks the save with an error "Retry count must be between 1 and 6". Given Retry Interval is outside 5 minutes–7 days, When I click Save, Then validation blocks the save with an error "Retry interval must be between 5 minutes and 7 days". Given the studio timezone is America/New_York, When retries are scheduled, Then all cutoff and interval computations use the studio timezone. Given the policy is edited after retries were scheduled, When those retries execute, Then they honor the policy version captured at decline time (not the newly saved policy).
Client Confirmation Requirement Toggle
Given Require Client Confirmation is On, When a soft decline occurs, Then a heads‑up message is sent immediately using the configured template and the auto‑switch is paused until the client confirms via one‑tap link. Given Require Client Confirmation is On, When the client taps Confirm within the policy window, Then the payment method is switched and charge/retry proceeds per policy and the action is logged with timestamp and client ID. Given Require Client Confirmation is On, When the client attempts to confirm after the confirmation window has expired, Then the client sees "Link expired" and no change is made. Given Require Client Confirmation is Off, When a soft decline occurs, Then the switch proceeds automatically without requiring client action and an FYI notification is sent. Given a confirmation is received, Then the audit trail records actor, IP, timestamp, and policy version applied.
Message Templates and Personalization
Given I open Fallback Pay Message Templates, When I edit Email and SMS templates for Heads‑up, Reminder, and Outcome, Then I can insert supported variables {client_name},{class_name},{start_time},{amount},{studio_name},{policy_summary_url}. Given a template contains an unsupported variable, When I click Save, Then validation highlights the token and blocks the save with "Unsupported variable". Given I click Preview on an SMS template, When preview renders, Then I see populated sample data and character count with segmentation warnings if >160 chars. Given Test Mode is On, When I click Send Test, Then the message is delivered to the specified test address/number and the event is logged as TEST with the template version used. Given I save template changes, Then a new template version is created with editor, timestamp, and change summary.
Exception Rules by Product, Price, and Segment
Given I create an exception rule with Product Type = Membership, Price >= 200 (studio currency), and Segment = VIP, When I set overrides (disable ACH, require confirmation, retry count = 1) and click Save, Then the rule appears active with a visible priority number. Given a soft decline on a VIP Membership priced at 250, When policy evaluation runs, Then the exception rule overrides apply instead of the default policy. Given multiple rules match an event, When precedence is resolved, Then the order applied is product+segment+price > product+segment > product+price > segment+price > product > segment > price and the selected rule is shown in the event log. Given I attempt to create a duplicate rule with identical conditions, When I click Save, Then the save is blocked with "An identical rule already exists". Given I disable or reorder a rule, When a new event occurs, Then the updated precedence is used and the change is captured in the audit log.
Test Mode and Sandbox Transactions
Given I toggle Test Mode On in Fallback Pay settings, When the page updates, Then a persistent "Test Mode" banner is visible and all Fallback Pay flows use sandbox payment processors with no live charges. Given Test Mode is On, When I trigger a simulated soft decline on a test booking, Then retries, confirmations, and messages execute in sandbox and all related events are labeled TEST in logs. Given I toggle Test Mode Off, When new Fallback Pay events occur, Then they run in Live mode while historic TEST events remain accessible in history. Given my role is not Admin, When I view the Test Mode control, Then I cannot enable/disable it (control is hidden or disabled). Given Test Mode is On, When I export events, Then TEST/LIVE flags are included in the export.
Policy Summary, Effective Dates, Versioning, and Per‑Class Overrides
Given I open Policy Summary, When the page loads, Then I see the current policy values, effective start/end timestamps, timezone, version number, and last modified by. Given I schedule a new policy with an Effective From date/time in the future, When I click Save, Then a new version is created in Pending state and will auto‑activate at the specified time while the current version remains active until then. Given I view Version History, When I select two versions, Then a diff view shows changes across all configurable fields and I can export the history to CSV. Given I am publishing a new class, When I open Advanced Settings and set per‑class overrides (toggle, allowed methods, retry/cutoff, confirmation requirement, template variant) and Save, Then those overrides apply only to that class’s transactions. Given a per‑class override exists, When policy evaluation runs for that class, Then the per‑class override takes precedence over default and exception rules and the event log notes "class override applied" with class ID. Given I remove a per‑class override and republish, When new events occur for that class, Then default policy evaluation resumes.
Security, Compliance, and Consent Logging
"As a studio operator, I want payment data secured and consents logged so that we remain compliant and can resolve chargebacks or disputes with confidence."
Description

Use gateway tokenization for all payment methods, never storing raw PAN or bank details on ClassTap systems. Enforce PCI-DSS SAQ A scope, encrypt all sensitive metadata at rest and in transit, and implement least-privilege access to vault references. Support SCA/3DS where required and step-up authentication on fallback capture when mandated. For ACH, collect and store NACHA-required authorizations and handle micro-deposit or instant verification flows as supported by the gateway. Maintain immutable audit logs for consent, policy applied, attempts, and captures with retention aligned to legal and studio requirements.

Acceptance Criteria
Tokenization of Payment Methods; No Raw PAN/Bank Data Stored
Given a client enters card, ACH, or wallet details at checkout When the payment method is submitted Then the gateway tokenization API is used over TLS 1.2+ to tokenize the details And no raw PAN, CVV/CVC, expiry, routing, or account numbers are stored or logged by ClassTap systems (DB, caches, logs, analytics, error trackers) And only token, brand, last4, and expiry month/year are stored And automated tests verify redaction in application logs and tracing for success and error paths
PCI-DSS SAQ A Scope Controls
- All payment inputs are handled via gateway-hosted fields/pages or PCI-compliant iframes; ClassTap never receives PAN/CVV in the backend or front-end JavaScript context - Static and dynamic code analysis confirms no PAN/CVV handling; egress/network policies block outbound transmission of card data to non-gateway domains - Quarterly ASV scans pass; annual SAQ A attestation and network diagrams are stored and accessible - Gateway webhooks contain only tokens/IDs and are verified via signature or mTLS; unverified webhooks are rejected with 401 - A PCI review checklist is required and attached for any change that touches checkout/payment flows
Encryption of Sensitive Metadata at Rest and In Transit
Given vault tokens, mandates, and auth metadata must be persisted When data is written to storage Then it is encrypted at rest using AES-256-GCM with keys in a managed KMS/HSM And key rotation occurs at least every 12 months or upon key compromise And all transport uses TLS 1.2+ with HSTS and TLS version enforcement And secrets and sensitive values are never logged and are masked in configuration UIs
Least-Privilege Access to Vault References
- Service accounts accessing vault tokens have read-only, scoped permissions; no human users have direct read access in production - Access to tokens requires service-to-service auth (mTLS or OAuth2 client credentials) with least-privilege scopes - Unauthorized access attempts are blocked, alerted within 5 minutes, and recorded in security logs - Break-glass access requires multi-party approval, is time-bounded, and is fully audited - KMS key policies restrict decrypt to the payment service only; attempts by other services fail
SCA/3DS and Step-Up on Fallback Capture
Given a soft decline indicating SCA/3DS is required on a card transaction When fallback payment capture is attempted Then the client is prompted with a one-tap flow to complete 3DS/SCA via the gateway And on successful challenge, the fallback method is charged and the original booking is retained And on failure or cancellation, no charge occurs, roster status is unchanged, and the user is notified And the 3DS version, DS reference, liability shift, and exemption outcome are recorded in the audit log
ACH Authorization and Verification Compliance
Given ACH is selected as a payment method When bank details are entered via the gateway's UI Then NACHA authorization text is displayed and explicit consent (checkbox) is captured with timestamp, IP, and device fingerprint And an authorization/mandate ID and PDF/HTML snapshot are stored immutably And if micro-deposits are used, two correct amounts must be confirmed before any debit attempt; if instant verification is used, a verification reference is stored And ClassTap stores no bank account or routing numbers; only tokens/mandate IDs
Immutable Audit Logs and Retention Policy
- Each event (consent, policy applied, tokenization, attempt, 3DS/SCA event, ACH authorization, capture, retention action) writes an append-only log entry with UTC timestamp, actor, event type, request ID, outcome, and token references (no raw sensitive data) - Logs are stored on WORM/immutable storage with integrity checks (hash chain or provider immutability) and are not editable by application users - Retention is configurable per studio (min/max per legal policy) and enforced automatically; expired entries are securely purged with a logged deletion event - Audit logs are exportable (CSV/JSON) by date range and filterable by customer, invoice, or event type - Access to audit logs is role-restricted; all access is itself audited
Waitlist, Membership, and Recurring Compatibility
"As a studio owner, I want fallback payments to work with waitlists, memberships, and recurring charges so that automation continues smoothly and clients keep their spots."
Description

Ensure Fallback Pay operates consistently across immediate bookings, auto-promotion from waitlists, membership renewals, package refills, and invoice-based payments. When a waitlisted client is promoted and the primary method soft-declines, hold the spot and initiate fallback per policy without breaking the waitlist chain. For recurring charges, respect billing cycles, grace periods, and proration while using stored backups. Provide safeguards to avoid double-charging when multiple automations trigger concurrently.

Acceptance Criteria
Immediate Booking Soft Decline with Fallback Pay
Given a client completes checkout with a primary payment method on file and at least one eligible backup method stored When the primary authorization or capture returns a soft decline Then the system auto-attempts the next eligible backup method per the configured priority within the configured retry window And the booking remains reserved until a successful capture or the configured payment hold window expires And a heads-up notification is sent to the client within the notification SLA with a one-tap confirm link when policy requires confirmation And exactly one successful charge is captured and associated with the booking; any overlapping attempts are idempotently skipped And hard declines do not trigger fallback and result in immediate failure per policy without holding inventory And the booking confirmation is issued only after a successful capture and the inventory is released immediately on final failure And all payment attempts, outcomes, and selected methods are logged with timestamps and identifiers for audit
Waitlist Auto-Promotion Soft Decline with Held Spot and Fallback
Given a waitlisted client is auto-promoted into an open spot and their primary payment attempt returns a soft decline When Fallback Pay is enabled for waitlist promotions Then the promoted spot is held for the configured hold window and the attendee is marked Pending Payment without breaking the waitlist order And the system auto-attempts the next eligible backup method per policy; if confirmation is required, one-tap confirmation satisfies the attempt And on successful capture, the attendee is confirmed and the roster remains intact with no reordering of the waitlist And if all backups fail or the hold window expires, the spot is released and offered to the next eligible waitlister automatically And at no time does class capacity exceed its limit and no duplicate charges are created for the same promotion event And all state transitions and payment attempts are auditable with correlation IDs linking waitlist event to payment
Membership Renewal with Backup and Grace Period
Given a member reaches their renewal timestamp and auto-billing is enabled with a stored primary and backup payment method(s) When the primary renewal charge soft-declines Then the membership remains active during the configured grace period and retries occur on the configured schedule using eligible backups per priority And upon any successful backup capture within the grace period, the renewal is recorded once, the cycle anchor remains unchanged, and any proration rules are applied exactly as defined for the plan And if no attempt succeeds by grace end, the membership is suspended per policy and no additional charges are attempted for that cycle And exactly one successful payment is associated to the renewal invoice; duplicate or partial duplicate captures do not occur And taxes, discounts, and ledger entries match the renewal invoice regardless of which payment method succeeded And notifications are sent for soft-decline, retry, success, and final failure within the defined SLAs
Package Refill Auto-Charge with Backup on Soft Decline
Given an account configured to auto-refill a package when credits reach or drop below the threshold and primary and backup methods are stored When the refill is triggered and the primary charge soft-declines Then the system attempts the next eligible backup method per priority within the configured retry window And on success, credits are granted exactly once and associated to a single refill order; duplicate credits are not issued under concurrent triggers And if all backups fail, no credits are granted and the refill order remains unpaid or is canceled per policy with a notification sent And all attempts are idempotent using the refill order as the charge subject to prevent double-charging And audit logs show the trigger source, attempt sequence, outcomes, and final state
Invoice Auto-Collect with Fallback Pay
Given an invoice reaches its due date/time with auto-collect enabled and the customer has a primary and at least one backup payment method When the primary auto-collect attempt soft-declines Then the system attempts the next eligible backup method per the configured priority and dunning policy And on successful capture, the invoice is marked Paid with a single payment and a receipt is sent; subsequent attempts are idempotently skipped And if no attempt succeeds, the invoice remains Open/Past Due and dunning continues per policy without creating multiple pending payments And when multiple invoices auto-collect in the same window, each uses a distinct idempotency key and no cross-invoice double-charging occurs And all attempts and outcomes are recorded with timestamps and method details
Concurrent Automations Double-Charge Safeguards
Given multiple automations for the same customer may fire concurrently (e.g., waitlist promotion, membership renewal, and package refill) When payment attempts are initiated for chargeable subjects Then the system generates a unique idempotency key per chargeable subject and acquires a short-lived lock to ensure only one capture can succeed And at most one successful charge is recorded per subject; all competing attempts exit as Skipped - Duplicate without any capture And financial records, receipts, and ledger entries reflect a single payment per subject And automated tests demonstrate zero double captures across high-concurrency simulations, and monitoring alerts on lock contention beyond the threshold And audit logs include the idempotency keys, lock outcomes, and the winning attempt ID
Roster Integrity and Waitlist Chain Preservation During Fallback
Given a class with capacity limits and a promoted attendee in fallback payment flow When the attendee is in Pending Payment state within the hold window Then the roster shows the attendee as Pending without exceeding capacity and instructors see accurate headcounts And if payment succeeds, the status becomes Confirmed; if payment fails or the hold expires, the slot is released and the next waitlister is notified per policy And the waitlist order is never altered by payment state and no orphaned holds persist beyond the hold window And analytics exclude Pending attendees from attended counts while including them in pending revenue metrics And all state transitions are idempotent and fully auditable with timestamps
Recovery Analytics and Exception Alerting
"As a studio owner, I want analytics on recovered revenue and alerts when action is required so that my team only intervenes when necessary."
Description

Deliver a dashboard and export that quantify revenue recovered by Fallback Pay, recovery rate by method type, soft-decline reasons, time-to-capture, and client confirmation engagement. Offer cohort views by studio, class type, and campaign. Emit events to the analytics pipeline and provide webhooks for recovered, switched, or failed states. Generate actionable alerts only when manual intervention is required (e.g., both methods fail, consent denied), with links to resolve in the booking or customer profile.

Acceptance Criteria
Revenue Recovery Dashboard Metrics
Given an account with Fallback Pay enabled and recovery data in the selected date range When a user with Analytics:View permission opens the Recovery Analytics dashboard Then the dashboard displays total_attempts, total_recovered_amount (account currency, 2 decimals), recovery_rate (%) And the dashboard displays breakdowns by method_type = {card, ach, wallet} And the dashboard displays soft_decline_reason distribution by reason_code and reason_label And the dashboard displays time_to_capture P50 and P95 in minutes And data latency from event capture to dashboard is ≤ 5 minutes And empty states render "No data" with contextual help when the result set is empty And all timestamps reflect the account time zone and show the UTC offset in the header And metric values reconcile with the data warehouse aggregated values within ±0.5% for the same range
Cohort Filters by Studio, Class Type, and Campaign
Given a user with Analytics:View permission When the user applies Studio, Class Type, and Campaign filters individually or in combination Then all widgets, tables, charts, and KPIs reflect the filtered cohort consistently And the applied filters persist in the URL and are restored on refresh and share And multi-select is supported for each filter with an explicit "All" option And zero-result states display counts of 0 without errors And default date range is Last 30 Days and can be changed to presets or custom range And the export function respects the active filters and date range And filter chip counts match the totals shown in the main KPIs
Recovery Metrics CSV Export
Given recovery data exists for the selected filters and date range When the user requests a CSV export from the Recovery Analytics dashboard Then a background job is enqueued and a downloadable file is provided within 2 minutes for ≤100k rows and within 10 minutes for up to 5M rows And the CSV is UTF-8 with comma delimiter and header row And the schema includes: booking_id, customer_id, studio_id, class_type, campaign_id, primary_method, fallback_method, attempt_id, attempt_at (ISO8601), status (recovered|switched|failed), soft_decline_reason_code, soft_decline_reason_label, recovered_amount_cents, currency, recovered_at (ISO8601), time_to_capture_ms, confirmation_channel, confirmation_at (ISO8601) And numeric fields are unformatted; timestamps are ISO8601 in account time zone And repeated exports with identical parameters produce identical content hashes And exports larger than 250MB are delivered via pre-signed URL valid for 24 hours And export jobs expose status: queued|processing|ready|failed with retry option on failure
Analytics Events Emission to Pipeline
Given a state change occurs in a Fallback Pay flow When the state transitions to recovered, switched, or failed Then an analytics event is emitted with event_name in {fallback_recovered, fallback_switched, fallback_failed} And the payload includes: event_id (UUIDv4), occurred_at (UTC ISO8601), account_id, studio_id, booking_id, customer_id, attempt_id, primary_method, fallback_method, status, soft_decline_reason_code, recovered_amount_cents, currency, time_to_capture_ms, confirmation_channel, confirmation_latency_ms, schema_version And events are emitted exactly-once per attempt_id and status via idempotency key And PII fields (customer_email, customer_phone) are excluded or SHA-256 hashed when included per privacy setting And events are delivered to the pipeline within 2 seconds of state change with at-least-once retry and exponential backoff up to 24 hours And event ordering is recoverable using occurred_at and an incrementing sequence_number per attempt
Webhooks for Recovered, Switched, and Failed States
Given a tenant has configured a webhook endpoint with a valid signing secret When a Fallback Pay attempt transitions to recovered, switched, or failed Then the system sends an HTTPS POST with JSON payload containing: id, type, created_at, data (booking_id, customer_id, attempt_id, primary_method, fallback_method, status, soft_decline_reason_code, recovered_amount_cents, currency, time_to_capture_ms, links {booking_url, customer_url}), schema_version And each request includes headers: X-CT-Signature (HMAC-SHA256), X-CT-Event-Id, X-CT-Idempotency-Key, X-CT-Timestamp And deliveries are retried with exponential backoff for non-2xx responses for up to 24 hours And idempotency guarantees that duplicate deliveries for the same X-CT-Idempotency-Key are ignored by our sender And endpoints returning 410 Gone are automatically disabled after 3 consecutive attempts and surfaced in the UI And a Test delivery can be triggered from settings and must succeed before enabling the endpoint
Actionable Exception Alerting Only When Intervention Required
Given a Fallback Pay attempt where both primary and fallback payment methods fail or the client denies consent When the exception condition is detected Then an alert is created in the in-app Alert Center with severity=high and reason in {both_methods_failed, consent_denied} And the alert includes deep links to the booking and the customer profile with pre-filled remediation actions And alerts are not generated for soft declines that auto-recover without intervention And duplicate alerts for the same attempt_id are deduplicated within a 24-hour window And closing the alert records an audit log entry with user_id, action, timestamp And optional email notification is sent only if the tenant has enabled email alerts
Client Confirmation Engagement Measurement
Given heads-up and one-tap confirmation messages are sent to clients during fallback flows When clients view and act on these messages Then the system records engagement events: confirmation_viewed, confirmation_clicked, confirmation_confirmed with timestamps and channel in {push, sms, email} And the dashboard displays open_rate and confirm_rate by channel, method_type, and cohort filters And multiple views within 15 minutes are counted once per client per attempt And opt-outs and unsubscribes are respected and excluded from denominator calculations And engagement metrics are available with ≤5-minute latency and match event counts within ±1%

ToneShift Dunning

Brand‑safe, multi‑channel reminders that adapt tone, frequency, and timing to each client’s history. Sends via email/SMS within local quiet hours, includes one‑tap payment‑method updates, and tracks a live status page—recovering more payments without damaging relationships.

Requirements

Adaptive Tone & Cadence Engine
"As a studio owner, I want reminders to adapt tone and frequency to each client’s history so that I recover more payments without damaging relationships."
Description

Rules-driven selection of reminder tone (friendly, neutral, firm) and send cadence per recipient using signals such as invoice age, amount, client tenure, past responsiveness, prior disputes/chargebacks, and preferred channel. Provides brand-safe guardrails via approved copy libraries, blocked phrases, and escalation limits. Exposes admin controls to configure tone boundaries, escalation steps, and per-segment strategies while defaulting to best-practice presets. Outputs a per-recipient sequence plan consumed by the delivery orchestrator, with previews and change logs for auditability. Supports template variants and lightweight A/B experiments to improve recovery without harming relationships.

Acceptance Criteria
Tone selection adapts by signals and thresholds
Given default preset thresholds (high_amount >= $500, new_client < 90 days tenure, dispute_flag = true) When the engine evaluates a recipient with invoice_age <= 7 days, amount < $500, tenure >= 6 months, and last_response_at <= 72 hours Then it selects tone = friendly and records the rule path used Given invoice_age between 8 and 21 days with no recent response When amount < $500 and no dispute_flag Then it selects tone = neutral Given invoice_age >= 30 days and two prior non-responses When no dispute_flag and amount >= $500 Then it selects tone = firm Given any recipient with dispute_flag = true or prior_chargebacks >= 1 When evaluating tone Then the maximum tone applied is capped at neutral unless an admin override exists Given a positive signal (payment intent or reply) within 72 hours When recomputing tone for the next step Then the tone does not escalate in this cycle
Cadence plan respects quiet hours, spacing, and frequency caps
Given default cadence presets (max_reminders_per_invoice = 4, min_spacing_hours: friendly >= 48, neutral >= 72, firm >= 96, weekly_cap_per_recipient <= 2) When generating a sequence plan for a recipient in timezone TZ Then all planned send_times avoid local quiet hours 21:00–08:00 TZ and satisfy the min spacing and weekly cap Given a recipient preferred_channel = SMS with email as fallback When no response is detected after the first step within the configured wait window Then the next step uses the fallback channel and maintains spacing rules Given a successful payment or payment-method update event When the plan is recomputed Then all future reminder steps for that invoice are canceled and the plan status is set to completed Given multiple invoices for the same recipient When generating plans Then sends are de-conflicted so no more than one reminder is scheduled within any 24-hour window across invoices
Brand-safe guardrails on templates, phrases, and escalation limits
Given an approved copy library and a blocked_phrases list When a sequence plan is generated Then every selected template_id exists in the approved library and no step content contains a blocked phrase Given escalation_limits (min_contacts_before_firm = 2, min_invoice_age_for_firm = 21 days) When computing steps Then no firm tone is scheduled before the limits are met Given a recipient segment flagged as VIP or Sensitive When generating tone Then the maximum tone allowed is neutral regardless of other signals unless an admin override is explicitly enabled
Admin config for tone boundaries, escalation steps, and per-segment strategies
Given an admin with permissions When updating tone caps (e.g., New Clients <= neutral), amount thresholds, or min_spacing values Then changes are validated (no negatives, logical ordering), saved, versioned, and applied only to plans generated after the change Given best-practice presets are available When an admin selects “Load Preset: Balanced Recovery” Then the UI populates default thresholds and cadence values and shows a diff preview before saving Given per-segment strategies (e.g., Tenure < 90 days, High Amount >= $500, Prior Chargeback) When saving segment rules Then each rule shows a priority and no overlapping rules without explicit priority are allowed
Per-recipient sequence plan output with preview and schema validation
Given recipient, invoice, and configuration inputs When generating a plan Then the output includes plan_id, version, recipient_id, invoice_id, and steps with {step_id, tone, channel, template_id, earliest_send, latest_send, reason_codes, experiment_id?} Given the plan output When validated against the published JSON schema Then it passes schema validation and is accepted by the delivery orchestrator contract Given the same inputs and config version When generating a plan twice Then the plan_id and step_ids are identical (idempotent) Given a selected step When rendering a preview Then the merged content displays with correct personalization tokens and no template errors
Change logs and audit trail for configurations and plans
Given any admin configuration change When the change is saved Then an audit record is created with {who, when, before, after, reason, config_version} and is immutable Given a plan generation or recomputation When it completes Then an audit entry records input signals, rules fired, output plan_id/version, and any suppressions applied Given an auditor view When filtering by recipient_id, invoice_id, date range, or actor Then matching audit entries are returned and can be exported to CSV and JSON
Template variants and lightweight A/B experiments with safeguards
Given a template with variants A and B and weights A=80%, B=20% When plans are generated Then recipients are randomly assigned per-invoice with stable assignment across steps and the aggregate assignment matches weights ±5% after 500 assignments Given experiment guardrails (max_experiment_exposure = 20%, complaint_rate_alert = 2x control with min N=100) When guardrails trigger Then variant B is auto-paused within 60 minutes and future steps fall back to control variant Given any variant content When validating before use Then it must be approved and pass all brand-safe guardrails (no blocked phrases, correct locale, schema-valid personalization tokens)
Localized Quiet Hours & Timezone Scheduling
"As an instructor, I want dunning messages to send only during each client’s local daytime hours so that reminders feel respectful and less intrusive."
Description

Automatically determines each recipient’s local timezone from profile, booking history, or geolocation heuristics and schedules emails/SMS within configurable quiet-hour windows (e.g., 8am–8pm local). Handles daylight saving changes, regional exceptions, and business-defined rules for weekends and holidays. Includes backoff and jitter for retries, rescheduling if a planned send falls into quiet hours. Exposes org-level and segment-level overrides and integrates with the orchestration layer to ensure all sends respect frequency caps and local regulations.

Acceptance Criteria
Timezone Resolution Fallback Chain
Given a recipient has profile.timezone set When the system schedules a dunning message Then profile.timezone is used to compute local send times and the IANA TZ ID and source=profile are recorded Given a recipient lacks profile.timezone but has prior bookings with known venue timezones When scheduling Then the timezone from the most recent successful booking is used and source=booking is recorded Given no profile/booking timezone but a recent IP geolocation with confidence >= 0.8 When scheduling Then the geolocated IANA timezone is used and source=geolocation is recorded Given no reliable timezone sources When scheduling Then the org default IANA timezone is used and source=org_default is recorded And in all cases offsets are computed at dispatch time from the IANA zone And if effective timezone changes prior to dispatch and scheduled time is > 24h away Then the message is rescheduled to preserve the same local wall-clock time
Quiet-Hour Enforcement for Email and SMS
Given org quiet hours 08:00–20:00 local for email and SMS When a send is requested at 19:59 local Then it is dispatched immediately Given the same configuration When a send is requested at 20:00 local Then it is scheduled for the next calendar day at 08:00 local Given a message is due at 07:30 local due to retry/backoff When quiet hours are 08:00–20:00 Then it is moved to 08:00 local And no email/SMS is dispatched outside the allowed quiet-hour window for the recipient’s local timezone And the applied window (start, end, timezone, source) is recorded with the message
DST Transitions and Ambiguous/Skipped Times
Given America/Los_Angeles spring-forward day with nonexistent 02:00–02:59 When a message is scheduled at 02:30 local Then it is adjusted to 03:00 local and reason=nonexistent_local_time_adjustment is logged Given fall-back day with duplicated 01:00–01:59 When a message is scheduled at 01:15 local Then it is dispatched once using the post-transition offset and not duplicated And quiet-hour boundaries are evaluated in local civil time regardless of DST shifts And all logs include local time, UTC time, and IANA timezone for traceability
Weekend/Holiday Rules and Regional Exceptions
Given a segment tagged region=KSA with weekend=Fri–Sat When a message would be sent on Friday within quiet hours Then it is deferred to Sunday 08:00 local unless the segment override allows weekend sends Given a region with a holiday on 2025-12-25 When a message would be sent that day Then it is deferred to the next business day at 08:00 local And admin-configured holiday calendars per region are respected And segment-level overrides can opt in to weekend/holiday sends and supersede org defaults
Retry Backoff with Jitter and Quiet-Hour Reschedule
Given a transient failure (e.g., HTTP 5xx, carrier throttle) When retrying Then exponential backoff is applied starting at 5m, doubling each attempt, capped at 6h, with ±20% jitter And maximum attempts per message are limited to 6; upon exceeding, the message is marked permanently failed And if a computed retry time falls outside allowed quiet hours Then it is moved to the next allowed send time while preserving attempt ordering And retries are idempotent via a stable message key so only one successful send is recorded
Orchestration, Frequency Caps, and Regulatory Compliance
Given per-recipient caps of max 2 dunning messages per 24h and 5 per 7 days across channels When a new send is evaluated Then it is blocked or deferred so caps are not exceeded, and the decision is logged with next eligible time Given US recipients for SMS When local time is outside 08:00–21:00 per TCPA Then SMS sends are blocked or rescheduled with reason=regulatory_window And when both email and SMS are eligible for the same minute Then orchestration selects the higher-priority channel per campaign rules and cancels the other And every decision is written to an audit log with recipient, rules hit, and next action timestamp
Org/Segment-Level Overrides and Precedence
Given org default quiet hours 08:00–20:00 and a segment override 09:00–19:00 When the recipient belongs to that segment Then the 09:00–19:00 window applies Given an admin updates a segment override When saved Then effective rules propagate within 5 minutes and apply to new schedules and reschedules Given removal of a segment override When saved Then recipients revert to org defaults on next evaluation And the effective configuration for any recipient can be retrieved via API including precedence: segment override > org default; timezone determination always prefers recipient profile when present
Multi‑channel Delivery Orchestration with Failover
"As a studio owner, I want reminders to automatically switch between email and SMS when appropriate so that clients actually see them without feeling spammed."
Description

Coordinated sequencing of email and SMS reminders with channel preferences, frequency caps, and deliverability-aware failover. Triggers SMS when email bounces or shows no engagement after a defined interval, and suppresses duplicate notifications across channels. Integrates with email and SMS providers via webhooks for delivery, open/click, reply, bounce, and unsubscribe events. Enforces per-recipient and global throttles, idempotency keys for safe retries, and deduplication across concurrent workflows. Provides per-tenant configuration for step timing, channel order, and maximum touches per invoice.

Acceptance Criteria
SMS Failover on Email Bounce
Given a reminder workflow for Invoice X with channel order Email then SMS and a valid mobile number on file And an email send attempt for Step 1 is initiated with idempotency key K1 When the email provider webhook posts a hard bounce event for that message Then the system schedules an SMS for Step 1 within the configured failover delay And marks the email step as Failed with reason "Bounce" And records the failover reason "Bounce" on the SMS send metadata And does not create more than one SMS for Step 1 even if duplicate bounce events are received And all actions respect per-recipient and global throttles and do not exceed configured maximum touches per invoice
No-Engagement Email to SMS Failover After Interval
Given a delivered email reminder for Invoice X with no open, click, or reply events recorded And the tenant has configured a no-engagement interval of T minutes for failover When T minutes elapse without engagement and the recipient has not unsubscribed Then an SMS reminder is scheduled within 1 minute of interval expiry And if any engagement event arrives before the SMS is sent, the SMS is suppressed And the workflow timeline and audit log record the failover trigger and suppression decisions And maximum touches per invoice are not exceeded
Channel Preference, Order, and Frequency Caps
Given tenant channel order is Email then SMS And the recipient’s channel preference is SMS preferred And per-channel frequency caps are configured as: max E email/day and max S SMS/day per recipient When a new reminder sequence for Invoice X is initiated Then the first attempt uses SMS (preference) if within SMS cap; otherwise uses Email if within Email cap And if both channels are at cap, the step is skipped and the workflow records "Frequency cap reached" And subsequent steps follow the configured order while honoring the recipient preference at each step And the system never exceeds the configured per-channel caps for the recipient on that day
Cross-Channel Duplicate Suppression
Given Email and SMS sends are scheduled for the same escalation stage of Invoice X for the same recipient When one channel changes status to Sent (provider accepted) for that stage Then the other channel’s send for that stage is automatically suppressed and marked "Duplicate suppressed" And no more than one notification is delivered for that stage across channels And suppression is reflected in metrics and the audit log with the winning channel recorded
Webhook Event Handling and Idempotency
Given the system receives a signed webhook request from an email or SMS provider containing an event of type delivery, open, click, reply, bounce, or unsubscribe with event_id E When the same event_id E is received multiple times Then processing is idempotent: engagement state, counters, and triggers are applied at most once And signature and timestamp validation pass before processing; invalid signatures are rejected with no side effects And unsubscribe events immediately halt future steps for the recipient for the related invoice and channel And reply events are recorded and suppress further reminders for that invoice if configured
Per-Recipient and Global Throttle Enforcement
Given per-tenant throttles are configured as: max touches per invoice N, and global send rate R messages/minute per tenant When scheduling any send for a recipient for Invoice X Then the system will not schedule if N has been reached for that invoice-recipient And if the tenant’s current send rate would exceed R, the send is queued and released when capacity is available And queueing preserves FIFO order within the tenant and records scheduled_at and released_at timestamps And no more than R messages are dispatched within any sliding 60-second window for that tenant
Idempotent Retries and Concurrent Workflow Deduplication
Given a send operation for a specific step uses idempotency key K When transient errors occur and the system retries with the same key K Then at most one provider send is created and recorded for key K And the final send record reflects either Sent or Failed with the consolidated attempt history And if two concurrent processes attempt to create the same step send (same recipient, invoice, channel, stage), only one send record is created and the other attempt no-ops with a dedup reason And audit logs capture deduplication and retry outcomes with correlation ID
One‑Tap Payment Method Update & SCA Handling
"As a client, I want a secure one-tap way to fix my payment method from a reminder so that I can resolve the issue quickly on my phone."
Description

Generates signed, expiring magic links that deep-link to a branded, mobile-first payment update page supporting tokenized cards, Apple Pay/Google Pay, and bank debit where enabled. Performs real-time 3D Secure/SCA challenges when required and updates the customer’s default payment method on success. Automatically retries the failed charge and closes the dunning loop, writing results back to the invoice and CRM. Enforces PCI SAQ‑A boundaries with hosted fields, TLS, short-lived tokens, and device fingerprinting. Supports issuer updater where available and records audit logs for all updates.

Acceptance Criteria
Magic Link Generation, Signing, and Expiry
Given a failed invoice requiring payment recovery and a customer needing to update their payment method When the system generates a one-tap magic link Then the link token is signed using a tenant-scoped rotating secret and encodes customer_id, invoice_id, and an expiry timestamp And the link expires after the configured TTL and becomes invalid after the first successful payment update, whichever occurs first And any request with an expired, tampered, or previously used token returns an invalid/expired link page and does not reveal invoice or customer existence And the link deep-links to the branded payment update page with prepopulated context and a nonce to prevent replay
Branded Mobile-First Payment Update Page (Hosted Fields, SAQ-A)
Given a customer opens the magic link on mobile or desktop When the payment update page loads Then the page renders with the merchant’s branding and is responsive across small screens to desktop And all card input fields are hosted by the PCI-compliant PSP (hosted fields) so that no PAN or CVC data touches ClassTap servers or logs And all network requests are performed over TLS 1.2+ with HSTS enabled And short-lived client tokens are requested just-in-time, scoped to the invoice/customer, and expire when idle
Payment Method Support and One‑Tap Wallet Flows
Given merchant payment method configuration and the user’s geo/device capabilities When the customer opens the payment update page Then only enabled and eligible methods are shown: tokenized cards; Apple Pay/Google Pay on supported devices/browsers; and bank debit where the merchant and region permit And Apple Pay/Google Pay present a one-tap sheet with the merchant name and amount due And if a wallet is unavailable, the flow gracefully falls back to card entry without error And on success, the PSP token for the selected method is created and set as the customer’s default payment method
Real-Time SCA/3DS Challenge Handling
Given the issuer or network indicates SCA is required for the new payment method or the retry When the customer confirms the update Then a 3DS v2 authentication is initiated via the PSP SDK supporting frictionless, challenge, and fallback to 3DS1 as needed And on SCA success, the method is marked verified and eligible to become default; on SCA failure, the default method is not changed and the user is prompted to retry or choose another method And timeouts, cancellations, and errors are captured, surfaced to the UI, and recorded with PSP result codes
Default Payment Method Update and Automatic Retry
Given a new payment method token is created and any required SCA has succeeded When the update is confirmed Then the customer’s default payment method is updated in the PSP and mirrored in the ClassTap CRM atomically And the previously failed invoice is retried immediately using the new default method And on successful capture the invoice is marked Paid and the dunning sequence is closed; on failure the invoice remains Unpaid/Open and the dunning sequence continues per policy And all outcomes are written back to the invoice record and customer timeline with references to PSP payment identifiers
Issuer Updater Auto-Refresh of Cards
Given a stored card is eligible for issuer updater/network token refresh When the PSP provides updated credentials (e.g., number, expiry, network token) Then the updated token is attached to the customer and set as default without requiring customer interaction And the failed invoice is retried as per the standard retry flow and outcomes are recorded And the audit log records the source as issuer updater and no magic link consumption is required
Comprehensive Audit Logging for Payment Method Updates
Given any attempt to update a payment method via magic link, wallet, manual entry, or issuer updater When the attempt completes (success or failure) Then an audit entry is recorded with timestamp, actor (customer ID), method type, masked details (brand, last4), IP, device fingerprint, user agent, SCA outcome, invoice ID, retry result, and PSP correlation IDs And audit logs are immutable, tenant-queryable, and redact sensitive fields so no PAN/CVC or full bank numbers are stored And access to audit logs is restricted to authorized roles and included in export for compliance
Live Invoice Status Page with Real‑time State
"As a client, I want a live status page linked from reminders so that I can see what went wrong and resolve payment without contacting support."
Description

Creates a secure, signed URL per invoice that shows amount due, reason for failure, last and next retry attempts, and available actions (update payment method, pay now). Reflects real-time changes via server-sent events or polling and inherits tenant branding and accessibility standards. Prevents indexing and sharing abuse with short-lived tokens and device-aware risk checks. Embeds contextual help and contact options, and records engagement analytics for recovery insights. Links are included in all dunning messages and respect consent and suppression rules.

Acceptance Criteria
Secure Signed Invoice URL Access and Expiration
Given a valid signed invoice URL within a 24-hour TTL from issuance, When the link is opened on the first device/browser to access it, Then the invoice status page loads and displays only that invoice’s data. Given an expired or tampered token, When the link is opened, Then the server returns 401/403, shows a generic “Link expired” page without invoice details, and provides a “Request new link” action. Given a token first used on Device A, When it is opened on Device B within the TTL, Then access is blocked with 403 and the token is invalidated; Device A retains access until TTL expiry. Given more than 3 failed token validations within 10 minutes from distinct IPs, When further requests arrive, Then the token is revoked and subsequent responses are 403. Given any request to the status URL, When the response is sent, Then X-Robots-Tag: noindex, nofollow and a meta robots noindex are present and no Open Graph tags expose invoice PII or amounts.
Invoice Details Display Accuracy
Given an unpaid invoice, When the page loads, Then it displays amount due with currency, the last failure reason (standardized code and human-readable text), the timestamp of the last retry, and the timestamp of the next scheduled retry with timezone indicator. Given no next retry is scheduled, When the page loads, Then it displays “No further retries scheduled” and shows “Pay now” and “Update payment method” actions. Given a paid or closed invoice, When the page loads, Then it displays a Paid state, amount due = 0, a receipt link or reference, and hides payment actions. Given the invoice currency uses minor units, When the amount is displayed, Then it is correctly rounded and formatted per locale and currency rules.
Real-time Status Updates
Given the status page is open and connected, When the invoice state or retry schedule changes, Then the UI updates within 3 seconds via server-sent events without a full page reload. Given SSE is unavailable, When the page is open, Then it polls at most every 15 seconds and reflects changes within that window. Given network connectivity is lost for more than 10 seconds, When the page remains open, Then a “Reconnecting…” banner is shown and upon reconnect the UI syncs within 5 seconds. Given the invoice becomes Paid while viewing, When the update is received, Then “Pay now” and “Update payment method” are disabled or hidden and a Paid confirmation is shown.
Payment Actions: Pay Now and Update Method
Given an unpaid invoice with at least one valid payment method, When the user clicks “Pay now,” Then an immediate payment attempt is initiated, a progress indicator is shown, and on success the page transitions to Paid with a receipt link; on failure the failure reason and next retry schedule update. Given an unpaid invoice, When the user clicks “Update payment method,” Then a secure PCI-compliant form renders in-page (iframe) for supported methods per tenant settings; on successful update a confirmation is shown and the next retry is scheduled within 60 seconds. Given SCA/3DS is required, When the user completes step-up authentication, Then the flow returns to the status page and reflects the final payment outcome. Given the user cancels payment or close the method form, When returning to the page, Then no duplicate charge is attempted and the page state remains consistent.
Branding and Accessibility Compliance
Given tenant branding is configured, When the page loads, Then tenant logo, primary/secondary colors, and typography are applied to header, buttons, and links per configuration. Given a keyboard-only or screen reader user, When navigating the page, Then all interactive elements are reachable in logical order, have visible focus states and ARIA labels, and error/success messages are announced via live regions. Given any text/background combination, When tested, Then contrast ratios meet WCAG 2.1 AA. Given a mobile viewport down to 320px width, When viewing the page, Then layout is responsive without horizontal scroll and touch targets are at least 44x44px.
Contextual Help and Contact Options
Given a known failure reason, When the user selects “Why did this fail?”, Then guidance specific to that reason is displayed along with tenant-configured contact options (email, phone, message) and expected response times or hours. Given the user selects a contact option, When the action triggers, Then the appropriate client opens (mailto, tel, or in-app message) and the interaction is logged without including PII in query strings. Given no failure reason is available, When help is opened, Then a generic explanation and contact options are shown.
Analytics and Dunning Link Inclusion with Consent
Given a dunning email or SMS is generated for an invoice with customer consent and not on suppression lists, When the message is sent, Then it includes the secure signed status URL with a 24-hour TTL. Given the status page is viewed, When the user performs key interactions (page_view, pay_now_click, pay_now_result, update_method_open, update_method_success, help_view, contact_click), Then events are recorded with timestamp, tenant_id, invoice_id, channel_ref, and device type and dispatched within 2 seconds; if consent is revoked, Then events are not sent or are anonymized. Given suppression or opt-out is active, When dunning messages are generated, Then no message containing the status link is sent.
Compliance & Consent Management for Email/SMS
"As a studio owner, I want consent and opt-outs handled automatically across channels so that my dunning stays compliant and trusted."
Description

Captures and enforces per-recipient consent for email and SMS, including opt-in sources, timestamps, and scope. Automatically honors STOP/UNSUBSCRIBE keywords and link-based opt-outs, immediately suppressing further sends and updating preferences across ClassTap. Applies country-specific regulations (e.g., TCPA, GDPR, CAN‑SPAM) including required disclosures, quiet-hour rules, and data retention. Maintains auditable logs of consent changes, message content, and send metadata. Provides APIs and UI for exporting/deleting personal data and for bulk suppression imports.

Acceptance Criteria
Per-Recipient Consent Capture and Enforcement
Given a recipient provides email and/or phone on any ClassTap surface (web, mobile, API, import) When they explicitly opt in selecting channel(s) and scope(s) Then the system stores a consent record with recipientId, channel, scope, source, timestamp (ISO 8601 UTC), ipAddress, userAgent (if web), jurisdiction (ISO country), consentTextVersion, and operator Given a send is attempted for a recipient without an active consent record matching the channel and scope When ToneShift initiates delivery Then the send is blocked, no provider call is made, and a "no_consent" suppression event is logged Given a consent change (grant or revoke) is made via any surface When saved Then it propagates system-wide within 15 seconds and supersedes prior records Given an API request to create/update consent is missing required fields or contains invalid values When processed Then the API responds 400 with a structured error and no consent is written
Real-Time STOP/UNSUBSCRIBE Suppression
Given an inbound SMS with any of STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, or QUIT (case-insensitive, trimmed) When received Then the recipient’s SMS preference is set to opted-out, a confirmation SMS is sent where permitted, and all future SMS are suppressed within 60 seconds Given a recipient clicks the email unsubscribe link When the link is visited Then the email channel is set to opted-out immediately, a confirmation page is shown, and future emails are suppressed within 60 seconds Given duplicate or repeated STOP/UNSUBSCRIBE actions When processed Then the system handles idempotently without error and logs a single suppression event per channel Given a HELP keyword SMS When received Then an informational SMS is returned with sender identity and opt-out instructions, complying with carrier requirements
Jurisdictional Quiet Hours and Required Disclosures
Given a recipient’s locale can be resolved (profile timezone, then E.164 country default, then billing address) When ToneShift schedules a message Then it does not send during the jurisdiction’s prohibited quiet hours and instead queues for the earliest permissible time, logging reason "quiet_hours" Given an SMS to a US number When composed Then the body includes sender identification and "Reply STOP to opt out" Given an email to any recipient When sent Then the footer includes sender legal name, physical postal address, and a one-click unsubscribe link Given a recipient’s locale is US When scheduling SMS Then messages are only sent between 8:00 AM and 9:00 PM recipient local time Given a jurisdiction-specific disclosure is required (e.g., GDPR controller identity link) When rendering the message Then the correct disclosure snippet for the recipient’s jurisdiction is injected
Auditable Consent and Message Logs
Given any send attempt (delivered or blocked) When processed Then an immutable log entry is recorded with recipientId, channel, scope, templateId/version, contentHash, personalizationHash, attemptTimestamp, outcome (delivered|blocked), outcomeReason, providerMessageId/response (if applicable), triggeringActor, regulationEvaluationSnapshot, and consentRecordIdUsed Given a consent grant, update, or revocation When saved Then a consent audit record is appended with previousValue, newValue, timestamp, source, operator, and ipAddress Given an authorized admin queries logs by recipientId and date range When requested via UI or API Then results return within 3 seconds for up to 10,000 records and can be exported as CSV/JSON Given data retention rules per jurisdiction are configured When a record exceeds its retention period Then personal data fields are purged or pseudonymized while preserving minimal audit proof, and the purge is logged
Data Export and Deletion (DSAR) via UI and API
Given an authenticated requestor with permission initiates a data export for a recipient When submitted via UI or API Then a machine-readable export (ZIP containing JSON/CSV of profile, consents, preferences, and message logs) is generated within 24 hours and a secure download link is provided Given a verified deletion request for a recipient When approved Then future sends are immediately canceled/suppressed, personal data is queued for deletion and removed within 30 days, and a minimal suppression token is retained to prevent reactivation Given an in-flight scheduled message exists for the recipient When deletion is initiated Then the message is canceled and logged with reason "recipient_deleted" Given any DSAR action completes When finished Then a completion receipt with timestamp and action details is logged and available for audit
Bulk Suppression Import with Global Preference Sync
Given an authorized admin uploads a CSV containing emails/phones for suppression When processed Then the file is validated (headers, formats), deduplicated, and a preview shows counts of valid, duplicate, and invalid rows Given a valid bulk suppression import up to 100,000 rows When confirmed Then all listed recipients are suppressed across channels specified within 5 minutes, queued messages are canceled, and a results report is generated Given rows with invalid formats (e.g., non-E.164 phone, malformed email) When processed Then those rows are rejected with line-level error messages and do not affect valid rows Given a suppression exists already for a recipient When re-imported Then no changes occur and an idempotent "already_suppressed" note is recorded
Scope-Aware Sending Policy for Dunning vs Marketing
Given a recipient has opted out of marketing but has no explicit SMS/email opt-in When ToneShift attempts to send a dunning message flagged as transactional Then the system evaluates jurisdictional rules and, where permitted, allows the send; otherwise it blocks and logs "transaction_not_permitted" Given a message is flagged as transactional dunning When rendered Then no promotional content is included, and required disclosures for the jurisdiction are present Given both marketing and transactional scopes exist for a channel When a marketing message is attempted to an opted-out recipient Then the send is blocked with reason "marketing_opt_out" even if transactional messages are allowed Given any scope evaluation occurs When deciding delivery Then the decision, inputs (consents, jurisdiction), and result are recorded in the audit log

GraceHold Countdown

After a failed installment, a visible countdown holds the seat for a configurable grace window. Clients can pay now, switch methods, or request a one‑time extension; when the timer ends, the system auto‑fills from the waitlist. Preserves fairness and reduces back‑and‑forth.

Requirements

Payment Failure Trigger & Grace Hold Creation
"As a studio owner, I want failed installments to automatically start a timed hold on the seat so that I don’t have to manually intervene and risk double-bookings or unfair releases."
Description

On any recurring installment or scheduled partial payment failure, the system automatically creates a grace hold for the affected booking, starts a countdown based on configured policy, and locks the seat from being released or rebooked while enabling limited actions (retry, change method, request extension). It logs the failure reason, associates the hold with the invoice and class occurrence, and ensures idempotency across payment provider webhooks to avoid duplicate holds. Hold state transitions are atomic and conflict-safe with waitlist and capacity management. APIs and events expose hold lifecycle to UI and downstream systems. The outcome is a consistent, fair process that reduces manual intervention and preserves class integrity.

Acceptance Criteria
Idempotent Grace Hold Creation on Payment Failure
Given a recurring installment or scheduled partial payment attempt for a booking fails When the failure is received via webhook or internal callback Then exactly one GraceHold is created for the booking and linked to the invoice and class occurrence And the GraceHold status is Active and the countdown starts immediately And the countdown duration equals the configured grace window for the organization at the time of failure And the failure reason, provider, attempt identifier, and timestamp are recorded on the hold and in the audit log And processing duplicate or out‑of‑order notifications for the same attempt does not create additional holds nor reset the timer And a HoldCreated event with holdId, bookingId, invoiceId, occurrenceId, startedAt, expiresAt, and failureReason is emitted once
Seat Locking and Allowed Actions During Active Hold
Given a GraceHold is Active for a booking’s seat When any other customer attempts to book the same class occurrence Then the system rejects the booking due to seat locked by Active Hold And the capacity counters do not decrease further and no waitlist promotion occurs And the original client can only perform: Retry Payment, Change Payment Method, or Request One‑Time Extension And cancel booking remains available to staff per policy but does not reallocate the seat until hold is Released or booking is canceled And all disallowed actions (e.g., transfer seat, manual rebook) are blocked with a clear error code
Countdown Initialization, Visibility, and Expiry Effects
Given a GraceHold is created When the UI queries hold details via API Then the API returns startedAt and expiresAt with second‑level accuracy and remainingSeconds consistent within ±1s of server time And a visible countdown can be rendered from these values without additional polling requirements beyond 30s When the expiresAt time is reached Then the hold transitions atomically from Active to Released And a HoldExpired event is emitted once And the seat becomes eligible for waitlist auto‑fill without overbooking And the hold record remains queryable with final status and timestamps
One‑Time Extension Request Handling
Given an Active GraceHold that has not been extended and policy permits extensions When the client requests an extension Then the system extends expiresAt by the configured extension window exactly once And the hold status remains Active and an ExtensionGranted flag is set And a HoldExtended event is emitted with previousExpiresAt and newExpiresAt When a second extension is requested for the same hold Then the request is rejected with a specific error code indicating one‑time limit reached and no change to expiresAt And all extension actions are logged with actor, reason (if provided), and timestamps
Audit Logging and Entity Associations
Given a payment failure triggers a GraceHold Then the audit log contains entries for FailureLogged, HoldCreated, and subsequent lifecycle changes with correlationId tying paymentAttempt, invoiceId, bookingId, occurrenceId, and holdId And the hold stores failureReasonCode, failureMessage (sanitized), providerName, paymentAttemptId, and retryCount And the invoice and booking records reference the active holdId while Active And all logs are redactable for PCI/PII and pass lint rules for structured logging
Concurrency and Conflict‑Safe State Transitions
Given concurrent events occur (e.g., duplicate webhooks, waitlist promotion, manual cancellation) while a hold is Active or expiring When these events are processed across multiple nodes Then only one terminal transition is committed per hold (Released or Canceled) and no double allocation of the seat occurs And capacity and waitlist counters remain consistent before and after transition And event emissions (HoldExpired, HoldReleased, BookingCanceled) are idempotent and delivered in causal order or with sequence numbers enabling downstream ordering And retries of failed operations do not create additional holds or allocate extra seats
Hold Lifecycle APIs and Events Exposure
Given platform and studio staff or UI clients need to query hold state When calling GET /holds/{holdId} Then the API returns: holdId, bookingId, invoiceId, occurrenceId, status [Active|Released|Canceled], startedAt, expiresAt, remainingSeconds, failureReasonCode, allowedActions, extensionGranted When listing holds by booking or occurrence Then the API supports filtering by status and returns paginated, consistent results And the system emits events for HoldCreated, HoldExtended, HoldExpired, HoldReleased with sufficient fields for downstream consumers And endpoints are authorized so only owners/staff can access sensitive details and PII is not exposed
Persistent Countdown Timer UI
"As a client, I want to see exactly how much time I have to keep my spot and how to resolve payment so that I can act confidently without contacting the studio."
Description

A visible, real-time countdown timer is displayed to the client on booking details, invoices, and payment screens across web and mobile, showing remaining grace time, available actions (Pay Now, Change Payment Method, Request Extension), and the consequences of expiry. The timer persists across sessions and devices using a server time-source with drift-safe updates, supports accessibility (ARIA live regions, sufficient contrast, screen-reader labels), and degrades gracefully with periodic sync under poor connectivity. It respects branding themes and can be embedded via share links. Edge cases are handled: pausing during manual review, instant reflection of extensions, and clear messaging when expired and the waitlist process begins.

Acceptance Criteria
Core Screens Countdown and Actions
Given a client with a failed installment and an active grace hold When they view booking details, invoices, or payment screens on web or mobile Then a countdown timer is visible above the fold, shows remaining time in mm:ss (or h:mm:ss when ≥1 hour), updates at 1 Hz, and displays actions: Pay Now, Change Payment Method, Request Extension, plus a concise expiry consequence message Given the timer is running When remaining time reaches 00:00 Then all payment/action buttons are disabled within 1 second, an "Hold expired" message is shown, and the waitlist handoff notice is displayed Given the user selects Pay Now When payment succeeds before expiry Then the seat is confirmed and the timer is removed within 1 second Given the user selects Change Payment Method When a new method is authorized before expiry Then the payment attempt uses the new method and the hold remains active
Branding and Embeddable Share Links
Given merchant branding is configured When the countdown renders Then it adopts brand typography and colors, buttons/styles match theme, and contrast ratios remain compliant Given white-label mode is enabled When the countdown renders Then no ClassTap branding is shown Given an embeddable share link is loaded in an external site (iframe or webview) When the countdown renders Then it is responsive, actions function end-to-end, uses the same server time source, and no cross-origin or mixed-content errors occur
Server Time Source and Drift Safety
Given the server is the authoritative time source When the client device clock is skewed by up to ±5 minutes Then displayed remaining time deviates by ≤1 second from server and self-corrects via sync Given stable connectivity When the countdown is visible Then the client re-syncs with server at least every 15 seconds and applies drift via smooth adjustments (≤200 ms per tick) Given delayed or batched updates When the server indicates remaining time ≤0 Then the UI clamps to 00:00 and triggers expiry handling within 1 second
Session and Device Persistence
Given a signed-in client has an active grace hold When they reload the page/app or sign in on a second device within the grace window Then the countdown resumes with correct remaining time from server and prior action availability is preserved Given the user is signed out When they open a share link to the booking with an active hold Then the countdown renders with the same remaining time and actions respect authentication (e.g., Pay Now prompts sign-in) Given a deep link from notification is opened When the target screen loads Then the countdown synchronizes with server within 2 seconds of view load
Connectivity Degradation and Resync Behavior
Given high latency (>800 ms RTT) or packet loss (>10%) When the countdown is visible Then the UI shows a non-blocking "Syncing…" indicator, continues a local countdown, and reconciles to server with correction ≤2 seconds on success Given the client is offline for up to 10 minutes When connectivity is restored Then the timer reconciles with server; if the hold expired while offline, the UI transitions to Expired within 2 seconds and disables payment actions Given repeated sync failures (≥3 consecutive) When the user attempts payment or extension Then a retry dialog is shown without duplicating requests, and telemetry logs include failure reason codes
Accessibility and Assistive Tech Support
Given screen reader usage When the countdown updates Then remaining time is exposed via aria-live="polite" and escalates to "assertive" at 60s and 10s thresholds with succinct labels (e.g., "60 seconds remaining") Given keyboard-only navigation When traversing the countdown and actions Then focus order is logical, focus indicators are visible, and activation of actions does not cause focus loss Given brand theming When the countdown renders Then text and controls maintain contrast ratio ≥4.5:1 and urgency is conveyed with non-color cues (icons/text) Given localization and RTL settings When user locale is applied Then time format and text are localized and RTL layouts render correctly without overlap or truncation
Extensions, Pauses, and Expiry Handoff
Given the client has not used their one-time extension When they tap Request Extension and confirm Then a modal explains the new total time, the server approves, remaining time updates within ≤1 second, the extension option disables thereafter, and an audit event is recorded Given an admin places the hold under Manual Review When the client views the timer Then a Paused state with reason is shown, actions are disabled except allowed messaging, and upon review end the timer resumes from the correct remaining time Given the hold expires When the timer reaches 00:00 (per server) Then the UI shows Expired messaging, disables actions, indicates waitlist auto-fill has started, and idempotent server calls prevent duplicate handoffs
Configurable Grace Window & Policies
"As an instructor, I want to set how long a seat is held after a failed payment and specify exceptions so that the policy fits my schedule and fairness standards."
Description

Admins can configure default grace durations at the account level with overrides at plan, class, and occurrence levels, including blackout periods near class start and holiday calendars. Policies define eligibility, maximum concurrent holds per user, whether holds pause outside business hours, deposit thresholds, grace start conditions (first failure only vs each retry), and seat-lock behavior when capacity is limited. Settings are auditable, versioned, exportable, and ship with safe defaults and inline guidance. These controls tailor fairness and operational fit across varied studio workflows.

Acceptance Criteria
Hierarchy: Account Default with Plan/Class/Occurrence Overrides
Given account default grace window = 24h and plan override = 12h and class override = 8h and occurrence override = 2h, When an installment fails for a booking in that occurrence, Then the applied grace window is 2h. Given account default = 24h and plan override = 12h and class override = 8h and occurrence has no override, When an installment fails for a booking in that class, Then the applied grace window is 8h. Given account default = 24h and plan override = 12h and class has no override and occurrence has no override, When an installment fails for a booking under that plan, Then the applied grace window is 12h. Given only account default = 24h and no overrides configured, When an installment fails, Then the applied grace window is 24h. Given a class with an override = 6h, When the admin views the class policy summary, Then the UI displays "Effective grace: 6h (source: class)".
Blackout Periods and Holiday Calendars
Given a blackout window of 60 minutes before class start, When an installment fails 30 minutes before start, Then no GraceHold is created and the seat is immediately released to waitlist auto-fill. Given a blackout window of 60 minutes before class start, When an installment fails 90 minutes before start and the user is otherwise eligible, Then a GraceHold is created with the configured duration. Given a holiday calendar with date D marked as a holiday and holiday holds disabled, When an installment fails on date D, Then no GraceHold is created and the seat follows the no-hold path. Given the holiday calendar and policy with holiday holds enabled, When an installment fails on a non-business holiday date, Then a GraceHold is created per configuration. Given an occurrence-level blackout override of 30 minutes and a class-level blackout of 60 minutes, When an installment fails 45 minutes before start for that occurrence, Then behavior follows the occurrence override (hold created).
Eligibility and Maximum Concurrent Holds
Given policy eligibility requires an active plan, When a user with an expired plan triggers an installment failure, Then no GraceHold is granted and the user sees an ineligible message. Given maximum concurrent holds per user = 2, When a user already has 2 active GraceHolds and triggers another failure, Then no new hold is created and the denial reason indicates the concurrency limit. Given maximum concurrent holds per user = 3, When a user has 2 active holds and triggers another failure, Then the 3rd hold is created and counted toward the limit. Given an admin updates the maximum concurrent holds from 2 to 1, When a user with 2 existing holds triggers another failure, Then the new hold is denied and existing holds remain unaffected.
Pause Behavior Outside Business Hours
Given business hours are 09:00–18:00 local and "pause outside business hours" is enabled, When a 60-minute GraceHold starts at 17:50, Then the timer runs 10 minutes, pauses at 18:00 with 50 minutes remaining, resumes at 09:00 next business day, and expires at 09:50. Given the same hours and "pause outside business hours" is disabled, When a 60-minute GraceHold starts at 17:50, Then it expires at 18:50 with no pause. Given a weekend is outside business hours, When a 2-hour GraceHold starts Friday at 17:00 with pausing enabled, Then 1 hour elapses by 18:00, the timer pauses for the weekend, resumes Monday 09:00, and expires Monday 10:00. Given a paused GraceHold, When the client views the timer, Then the UI indicates "Paused" and shows the scheduled resume time.
Deposit Thresholds and Grace Start Conditions
Given a deposit threshold of 20% and a booking price of $100, When the user’s paid deposit is $15 at failure time, Then no GraceHold is granted and the UI indicates deposit below threshold. Given a deposit threshold of 20% and a booking price of $100, When the user’s paid deposit is $20 at failure time, Then a GraceHold is granted. Given grace-start condition = "first failure only", When subsequent retry attempts fail within the same billing cycle, Then the GraceHold timer does not restart. Given grace-start condition = "each retry", When a retry fails while a hold exists, Then the timer restarts to the full configured duration and the event is recorded.
Seat-Lock Behavior with Limited Capacity and Waitlist Auto-Fill
Given class capacity is 10, 10 seats are filled, and seat-lock policy = "lock during hold", When a GraceHold is created for a participant, Then the seat remains reserved for the hold duration and no waitlist auto-fill occurs until expiration or cancellation. Given the same capacity and seat-lock policy = "release to waitlist", When an installment fails, Then no GraceHold is created and the system immediately attempts to auto-fill from the waitlist. Given seat-lock policy = "lock during hold", When the hold expires with no payment, Then the system automatically charges the next eligible waitlisted client per waitlist-to-payment rules and transfers the seat. Given seat-lock policy = "lock during hold", When a hold is manually canceled by an admin, Then waitlist auto-fill executes immediately.
Audit, Versioning, Export, Safe Defaults, and Inline Guidance
Given a new account, When the admin first opens GraceHold Policies, Then all fields are prepopulated with system safe defaults and inline guidance tooltips are visible for each setting. Given an admin changes a policy value at any scope, When the change is saved, Then an audit record is created capturing actor, timestamp, scope, old value, new value, and reason (if provided). Given multiple changes over time, When the admin views version history, Then chronological versions with IDs and effective-from timestamps are listed and the current version is indicated. Given the admin exports policies, When export format CSV is selected, Then the file downloads and includes current effective values per scope and a complete audit trail of changes. Given a class policy page, When hovering over a guidance icon, Then the help text describes the setting and links to documentation.
Payment Retry & Method Switch Flow
"As a client, I want to pay now or swap my payment method during the grace period so that I can keep my spot without delays."
Description

Clients can immediately retry the failed installment or switch to an alternate payment method within the grace window. The flow pre-fills the outstanding invoice, validates method eligibility (e.g., disallow ACH close to class start), and supports SCA/3DS challenges. On success, the hold clears and the booking is restored; on failure, the timer continues with explicit error feedback and next-step guidance. The system supports tokenized vaulting, proration if class timing requires, and integrates with PSP webhooks for definitive settlement, using idempotent retry endpoints to prevent duplicate charges.

Acceptance Criteria
Successful Immediate Retry with SCA
Given a failed installment has an active grace hold with remaining time When the client selects "Pay Now" to retry with the same stored card that requires SCA/3DS Then the system initiates the SCA/3DS challenge through the PSP and prevents duplicate submissions during the challenge And upon successful authentication and PSP authorization, the system waits for settlement confirmation via webhook before marking the invoice as paid And once settlement is confirmed, the hold is cleared, booking status returns to Active, the countdown disappears, and a payment receipt is sent And subsequent clicks or retries with the same attempt idempotency key do not create additional charges
Method Switch with Eligibility Rules (ACH Restriction)
Given a failed installment within the grace window When the client chooses "Switch Payment Method" Then the system lists only eligible methods based on configuration and timing, including disallowing ACH within the configured start-time cutoff window And ineligible methods are disabled with an explanatory message indicating the cutoff reason And upon selecting an eligible method, the client can complete payment without leaving the flow unless a PSP redirect is required
Invoice Pre-Fill and Proration Accuracy
Given the client enters the retry/switch flow from a failed installment When the form loads Then the outstanding invoice is pre-filled with the exact amount due including taxes/fees/discounts and any applicable proration based on class timing configuration And the amount displayed in the UI matches the amount sent to the PSP to within the smallest currency unit And a human-readable line item summary is shown before confirmation
Failure Handling, Timer Continuation, and Guidance
Given a payment attempt fails due to decline, network error, or SCA abandonment When the PSP returns a failure/requirement status or timeout Then the countdown continues without resetting, and no seat release occurs And the client sees a clear, friendly error mapped from the PSP code with next-step guidance (retry, switch method, update card, request one-time extension) And if one-time extension policy is enabled and unused, a "Request Extension" action is available and updates the timer upon approval
Webhook-Driven Settlement and Idempotent Retries
Given the PSP may authorize before settling When a client completes an authorization Then the system shows a "Processing payment" state until a success webhook is received, without marking the invoice as paid prematurely And duplicate client submissions or repeated webhooks for the same attempt are handled idempotently using a consistent key, resulting in a single charge and a single receipt And if a failure webhook is received after an apparent client-side success, the UI updates to failed state, the timer continues, and the client is prompted to retry or switch methods
Tokenized Vaulting and Default Method Controls
Given the client adds a new card or wallet during method switch When the client consents to save the method Then the method is vaulted as a PSP token, displayed in masked form, and optionally set as default if the client toggles "Make default" And vaulting is not required to complete the immediate payment And saved methods are available for future retries subject to eligibility rules
Hold Clearance and Waitlist Interaction
Given a grace hold is active on the seat When payment settles successfully within the window Then the hold is cleared, the booking remains assigned to the client, and any pending waitlist auto-fill for that seat is canceled And when the grace window expires without settlement, the seat is released and the waitlist auto-fill flow is triggered for the next eligible attendee, and the original client sees "Hold expired" with options to join waitlist or choose another time
One-Time Extension Request Workflow
"As a client, I want to request a one-time extension when something goes wrong with my payment so that I have a fair chance to resolve it."
Description

Within the countdown, eligible clients can request a single extension of a configurable duration. Rules determine auto-approval versus manual review by staff, with SLA timers and queueing. The system tracks one-time usage per booking and per user, prevents chaining, and updates the countdown instantly on approval. Staff tools surface relevant context (payment history, attendance, notes) and provide quick actions with templated responses. Denials retain the original expiry; approvals extend the hold and adjust waitlist notifications accordingly. All actions are logged for audit and reporting.

Acceptance Criteria
Eligible Client Sees Extension Option During Countdown
Given a booking with an active GraceHold countdown and the client is eligible for a one-time extension and has not used their user-level extension, When the client views the booking page or payment modal during the countdown, Then the "Request Extension" control is visible and enabled and displays the configured extension duration and remaining countdown time. Given the countdown has expired or the seat has been released or the client is ineligible (extension already used at booking or user level), When the client views the booking page or payment modal, Then the "Request Extension" control is hidden or disabled with an explanatory message and no request can be submitted. Given the extension feature is disabled by configuration, When any client views the booking page, Then the "Request Extension" control is not shown.
Auto-Approval Applies and Countdown Extends Instantly
Given auto-approval rules are configured and the client meets all configured criteria, When the client submits a one-time extension request during an active countdown, Then the system auto-approves within 1 second, extends the hold by the configured duration from the current expiry, updates the countdown immediately, marks the booking and user extension as consumed, sends the configured confirmation template, and reschedules any waitlist notifications to the new expiry. Given a client submits the same extension request multiple times within 60 seconds, When the system processes the requests, Then the action is idempotent using a request identifier and only one approval is recorded and applied.
Manual Review Queue with SLA and Staff Actions
Given the client does not meet auto-approval criteria, When they submit an extension request during an active countdown, Then the request is added to the Staff Extension Queue with an SLA timer per configuration and visible fields: booking details, remaining countdown, payment history (last 6 months), attendance rate, no-show count, client notes, and prior extension usage. Given a staff member opens the queue item, When they choose Approve and select a duration within the configured maximum, Then the hold is extended immediately, the countdown updates, usage flags are set, waitlist notifications are rescheduled, and the client receives the approval template. Given a staff member chooses Deny with a reason code, When they submit the decision, Then the original expiry is retained, the client is notified with the denial template including the reason, and the queue item is closed. Given the SLA elapses without a decision, When the SLA breach occurs, Then the queue item is flagged overdue and escalated per configuration without changing the original expiry.
One-Time Usage Enforcement and No Chaining
Given a client has already consumed their one-time extension for this booking or at the user level, When they attempt to request an extension via UI or API, Then the request is rejected with HTTP 409 Conflict and a message indicating prior usage, and no state changes occur. Given a booking has already received an extension approval, When the client views the booking during the extended countdown, Then the "Request Extension" control is not available for that booking. Given an extension request has been denied, When the client attempts another extension request for the same booking before expiry, Then the request is rejected and the client is instructed to complete payment or wait for expiry (no chaining).
Denial Retains Original Expiry and Waitlist Timing
Given an extension request is denied (auto or manual) while the countdown is active, When the denial is saved, Then the booking hold expiry timestamp remains unchanged, any existing waitlist auto-fill schedule and notifications are unaffected, and the client sees the unchanged remaining time. Given an extension request is denied after the countdown has already expired, When the denial is saved, Then no changes occur and the client is informed that the seat may have been released to the waitlist.
Waitlist Notifications Adjust on Approval
Given an extension request is approved while a waitlist exists, When the hold is extended, Then the system pauses or reschedules any pending waitlist release notifications to the new expiry without sending duplicate or premature alerts and preserves the waitlist order. Given only one extension is permitted, When an approval is applied, Then any subsequent schedules are based solely on the latest approved expiry and no additional extensions can be scheduled.
Comprehensive Audit Trail and Reporting
Given any extension request is submitted, approved, denied, or expires, When the event occurs, Then the system creates an immutable audit log entry containing: requestId, bookingId, userId, actor (client|system|staffId), decision (requested|autoApproved|approved|denied|expired), reasonCode or ruleId (when applicable), previousExpiry, newExpiry, createdAt (UTC), channel (web|mobile|api), and IP/userAgent (when available). Given audit logs exist, When an admin views the booking timeline or exports reports, Then all related extension events are visible in chronological order and exportable via CSV and API with the fields above.
Auto-Waitlist Reallocation on Expiry
"As a studio owner, I want seats to be automatically offered to the waitlist when a grace period ends so that classes stay full without manual effort."
Description

When the countdown expires without successful payment or an approved extension, the system atomically releases the seat and offers it to the next qualified waitlisted client based on configured rules (FIFO or priority tiers). It sends a payment link with an accept-by timer, marks the original booking as lapsed with reason codes, and handles any partial funds per refund policy. The process is resilient to race conditions and ensures capacity integrity via transactional locks, supports batch processing for multiple expiries, and emits events for analytics and messaging.

Acceptance Criteria
Grace Expiry Triggers FIFO Offer
Given a booking with active GraceHold and waitlist_rule=FIFO When the countdown expires without successful payment and without an approved extension Then the system releases exactly one seat in a single committed transaction And selects the next qualified waitlisted client by FIFO And creates a WaitlistOffer with a unique payment link and accept-by timer equal to the configured offer_window And sends the payment link notification to the client within 5 seconds of expiry And reserves exactly one seat for the offered client until the accept-by timer elapses or the offer is accepted And writes an audit log entry for release, selection, and notification timestamps
Priority Tiers Offer Ordering
Given waitlist_rule=PRIORITY_TIERS with tiers configured When a seat is released on grace expiry Then the system selects from the highest non-empty tier first And applies FIFO within that tier And if multiple clients share identical tier and enqueue timestamp Then a deterministic tie-breaker of ascending client_id is applied And only the chosen client receives the initial offer
Original Booking Lapse and Reason Codes
Given a booking lapses due to grace expiry processing When the expiry job runs Then the booking status is set to Lapsed And lapse_reason is one of [EXPIRED_NO_PAYMENT, EXTENSION_DENIED] And lapse_at is recorded in UTC with millisecond precision And the admin timeline shows the status change with actor=system and correlation_id And the record is included in reporting exports within 5 minutes And no further payment attempts are made on the lapsed booking
Partial Funds Settlement Compliance
Given partial funds exist on the lapsed booking When refund_policy=REFUND_TO_SOURCE Then a refund for the partial amount is created against the original payment intent with an idempotency key And the refund is submitted within 24 hours and its status is tracked to completion When refund_policy=CONVERT_TO_CREDIT Then an account credit equal to the partial amount is created and linked to the client profile with a unique credit_id And a client notification is sent for the settlement outcome in either case
Capacity Integrity under Concurrency
Given two or more workers detect the same expiry concurrently When they attempt to release and reallocate the same seat Then exactly one allocation is committed and all other attempts are no-ops due to transactional locks And total allocated + available seats equals capacity before and after processing And lock duration per seat does not exceed 200 ms under normal load And retries use idempotency keys so repeated executions do not create duplicate offers or refunds
Batch Processing of Simultaneous Expiries
Given N expiries (N<=500) occur within a one-minute window When the batch processor runs Then all expiries are processed without violating waitlist ordering rules or capacity constraints And processing completes within 60 seconds for N=100 in the standard environment And each expiry produces exactly one terminal outcome: WaitlistOfferCreated or ReleasedToPublic when no qualified waitlist exists And failures are retried up to the configured limit with no partial allocations left dangling
Event Emission and Messaging
Given a seat is released and reallocated due to grace expiry When the transaction commits Then events SeatReleased, BookingLapsed, WaitlistOfferCreated, and RefundInitiated (if applicable) are emitted And each event payload includes booking_id, seat_id, client_id (if any), reason, timestamps, correlation_id, and idempotency_key And events are published to the event bus within 5 seconds with at-least-once delivery guarantees And downstream notification templates for client and instructor are triggered accordingly
Multi-Channel Notifications & Reminders
"As a client, I want clear reminders about my grace period and what to do next so that I don’t accidentally lose my spot."
Description

The system sends templated notifications at key milestones: grace start, time-based reminders, extension request outcomes, and seat release. Channels include email, SMS, and push where available, with throttling, quiet hours, and user preferences. Messages contain deep links to pay, change method, or request extension and show the exact expiry time in the recipient’s timezone. Templates are brandable per studio with merge fields, and deliverability is monitored with retries on failures. Compliance with consent and regional regulations is enforced.

Acceptance Criteria
Grace Start Notification Dispatch
Given a client's installment payment fails and a GraceHold countdown starts When the grace window begins Then a "Grace started" notification is dispatched within 60 seconds via all channels allowed by the client's preferences and consent (email, SMS, push where available) And the message includes deep links to Pay Now, Change Payment Method, and Request One-Time Extension And the message displays the exact expiry date/time converted to the recipient's timezone with timezone abbreviation And studio branding and template customizations are applied And the send is logged with a correlation ID and recipient/channel details
Reminder Cadence with Throttling and Quiet Hours
Given a grace hold is active And the studio has configured reminder offsets (e.g., 24h, 4h, 30m before expiry) When reminder times are reached Then each reminder is queued to send within ±1 minute of the target time And no more than one reminder per channel is sent to the same recipient within any 60-minute window (throttling) And if a reminder would fall within the recipient's quiet hours, it is deferred to the first allowed minute after quiet hours And reminders honor channel preferences/consent, include deep links, and show the expiry in the recipient’s timezone And duplicate reminders caused by retries are suppressed using an idempotency key per recipient+event+offset
Extension Request Outcome Messaging
Given a client requests a one-time extension via a notification deep link When the extension request is submitted Then the client immediately receives an acknowledgment via their highest-priority allowed channel And when the extension is approved, the client receives an approval message containing the new expiry date/time in their timezone and updated deep links And when the extension is denied, the client receives a denial message including the reason (if provided) and deep links to Pay Now and Change Method And only one outcome notification is sent per request (idempotent on request ID)
Seat Release and Waitlist Auto-Fill Alerts
Given a grace hold expires without successful payment When the seat is released Then the original client is notified of seat release within 60 seconds via allowed channels And the first eligible waitlisted client is notified with an offer that includes the hold expiry time in their timezone and a deep link to complete payment And if the waitlisted client declines or times out, the next waitlisted client is notified under the same rules until the class is filled or the waitlist is exhausted And all notifications respect preferences, consent, throttling, and quiet hours
Brandable Templates and Merge Fields
Given a studio configures notification templates When the template is saved Then the system validates required merge fields are present: client_name, class_title, expiry_local, pay_link, switch_method_link, request_extension_link, studio_brand And a preview renders with sample data showing correct branding and timezone-converted expiry And if any required field is missing, save is blocked with an error listing the missing fields And if optional fields are absent, defaults are applied without blocking And deep links resolve to the intended flows in a test preview environment
Deliverability Monitoring and Retry Policy
Given a notification send is attempted When the provider returns a transient failure Then retries occur per default policy: Email up to 3 attempts over 30 minutes (exponential backoff), SMS up to 2 attempts over 10 minutes, Push 1 retry after 2 minutes And on permanent failures (e.g., hard bounce, invalid number, revoked push token), no further retries occur and the channel is marked undeliverable for that recipient And delivery status, attempt count, and final outcome are recorded and viewable in the studio message log And idempotency keys prevent duplicate visible messages across retries per recipient+channel+event
Consent, Preferences, and Regional Compliance Enforcement
Given per-recipient channel preferences and applicable regional regulations When evaluating a notification for send Then the system sends only to channels with verifiable consent and allowed by regional rules at that time And SMS includes STOP/HELP instructions and honors STOP within 60 seconds; email includes an unsubscribe link that honors opt-out within 5 minutes And quiet hours and do-not-disturb are enforced per recipient locale/policy And an audit log records the consent state and compliance checks used for each send decision, and messages are suppressed with a logged reason when no compliant channel exists

PlanSwap

Allow mid‑stream changes between deposit, pay‑in‑X, or pay‑in‑full with automatic proration and rescheduled dues. Optional pause and catch‑up rules keep committed clients enrolled instead of canceling, stabilizing cash flow and cohort retention.

Requirements

Mid-Stream Plan Swap Engine
"As a studio owner, I want to change a client’s active payment plan without canceling their enrollment so that I can keep them in the cohort while adjusting how and when they pay."
Description

Implements the core capability to switch an active enrollment between deposit, pay‑in‑X, or pay‑in‑full plans without canceling the booking. On swap, the engine recalculates the remaining obligation, applies credits from prior payments, generates a replacement payment schedule, and preserves enrollment and attendance records. It handles immediate charges or deferred charges based on merchant policy, aligns due dates to studio preferences, maintains seat reservations, and prevents double-bookings. The flow must be idempotent, reversible by admin rollback, and fully compatible with existing booking, waitlist, and autopay subsystems to ensure continuity of service and cash flow.

Acceptance Criteria
Swap Pay-in-X to Pay-in-Full with Immediate Charge
Given an active enrollment on a pay-in-4 plan with a valid default payment method When the admin swaps the plan to pay-in-full with policy "charge immediately" Then the engine computes the net amount = total contract price − payments received − refundable discounts + applicable taxes/fees per studio policy (rounded to the nearest cent) And applies prior payments as credit and generates an immediate charge for the computed net amount And voids all future installments and replaces the schedule with zero future installments And sends a payment receipt and updated schedule confirmation to the client and admin And records an audit log with before/after schedules, calculation breakdown, actor, timestamp, and idempotency key And the operation completes within 5 seconds at p95
Swap Deposit to Pay-in-X with Deferred First Installment
Given an active enrollment on a deposit plan with no future installments and studio rule first_due_alignment = "1st of next month" in the studio timezone When the admin swaps the plan to pay-in-6 with policy "defer first installment" Then the engine creates a schedule of 6 installments starting at 9:00 AM on the next 1st of the month in the studio timezone And no immediate charge is attempted And an autopay mandate is created/updated to the client’s default payment method for the new installments And a schedule confirmation is sent to the client and admin including the next due date and amount And the old deferred placeholders (if any) are canceled to avoid duplicate attempts And the operation completes within 5 seconds at p95
Proration and Credit Application on Mid-Cycle Swap
Given a mid-cycle swap between plans with different prices and fees and studio policy proration_mode = "sessions_remaining" When the engine recalculates the remaining obligation Then it computes prorated base = new_plan_price × (sessions_remaining ÷ total_sessions) and subtracts payments received and refundable discounts, adds applicable taxes/fees And applies rounding to 2 decimal places using round half up And marks non-refundable fees as non-creditable when configured And stores a calculation ledger with inputs, formula, and resulting amounts And the net amount due matches the expected value from the policy within ±$0.01
Preserve Enrollment, Attendance, and Seat Reservation
Given an active booking with attendance history and a reserved seat When a plan swap is executed Then the booking ID remains unchanged and no cancellation event is emitted And the reserved seat remains held for all future sessions in the enrollment And past attendance records and check-ins remain intact and unmodified And the client portal shows a continuous enrollment with an updated payment plan but the same class/cohort And the studio calendar and roster reflect no change other than the payment schedule
Idempotent Swap and Concurrency Safety (No Double-Booking)
Given a swap request sent with idempotency key K and retried multiple times within 10 minutes When the requests are processed Then exactly one payment schedule mutation is committed and at most one charge is captured, and all retries return the same result payload And external events/webhooks related to the swap are emitted once Given two admins initiate different swaps on the same enrollment concurrently When the system processes both Then operations are serialized on the enrollment; at most one swap succeeds and the other returns a conflict without side effects And the booking count for the student/class remains 1 and no additional reservation is created
Admin Rollback of Completed Swap
Given a completed swap within the rollback window defined by policy (e.g., 30 days) and the admin has rollback permission When the admin performs a rollback on the swap Then the prior payment schedule is reinstated with original due dates and amounts And charges captured as part of the swapped plan are refunded or converted to credits per studio refund policy and reconciled against the reinstated schedule And autopay mandates are restored to the prior schedule And the client and admin receive notifications detailing the rollback and its financial effects And an audit log links the swap and rollback operations and is idempotent on retry
Autopay and Waitlist Compatibility Post-Swap
Given a client with an active autopay mandate When a swap modifies the payment schedule Then autopay continues using the existing token without requiring re-consent unless the mandate type changes, and the next run uses the updated due dates and amounts And any previously scheduled payment attempts for the replaced schedule are canceled to prevent double charging Given a client on a waitlist with auto-promote enabled and a pending swap instruction When the client is promoted to a booked seat Then the generated payment schedule uses the swapped plan terms and triggers the correct initial charge per policy And waitlist-to-payment automation runs once without duplicate invoices or charges And events fire in order: swap.created (if applicable) -> schedule.created -> invoice.created -> payment.attempted
Proration & Balance Reconciliation
"As an admin, I want accurate proration and clear balance breakdowns during a swap so that clients are charged fairly and I can explain the math with confidence."
Description

Provides deterministic calculations for proration and outstanding balance during a plan swap. Supports time‑based and session‑count products, partial period usage, deposits, discounts, taxes, fees, and rounding rules. Automatically converts consumed value into earned revenue and remaining value into a new principal, applies credits/refunds where needed, and enforces minimum charge thresholds. Calculation results must be transparent via a breakdown preview, consistent across UI, API, and receipts, and audited for accuracy across gateways (e.g., Stripe).

Acceptance Criteria
Time-Based Proration: Pay-in-Full to Pay-in-3 Mid-Cycle
Given an active time-based product with total duration D, base price P, tax behavior T (inclusive/exclusive), discount rules DR, and fee configuration F And elapsed time t has been consumed at the swap moment And the target plan is Pay-in-3 with schedule S starting on swap date When a plan swap is requested Then consumed_value = prorate_time(P, t/D, DR, T) is marked as earned revenue And remaining_value = P - consumed_value becomes the new principal before applying target plan DR, T, and F And target plan installments over S are recalculated from remaining_value with DR, T, and F applied And rounding rules per org settings are applied deterministically and totals reconcile to currency minor units And any installment below minimum_charge_threshold is merged forward or collected at swap per policy, preserving the grand total And UI preview, API response, and receipt preview show identical amounts and schedules to the cent
Session-Based Proration: Deposit to Pay-in-Full After Partial Use
Given a session-based product with N total sessions, base price P, and a paid deposit amount D with refundable/nonrefundable policy PR And the customer has consumed c sessions at swap And discounts, taxes, and fees are configured When swapping to Pay-in-Full Then consumed_value = prorate_sessions(P, c/N) is marked as earned revenue And the nonrefundable portion of D is counted toward earned revenue; the refundable portion is applied to remaining_value first And if refundable deposit exceeds remaining_value, a credit/refund is created; otherwise, balance due is collected immediately or scheduled per policy And taxes and discounts are recomputed on the final chargeable amounts per tax behavior And minimum_charge_threshold is enforced for any installment And UI, API, and receipts display identical amounts and session counts
Discount & Tax Reconciliation on Plan Swap
Given the original plan has discount DO (amount/percent with defined duration/scope) and the target plan has discount DN And tax behavior T and jurisdiction J are configured When performing a swap Then discounts apply to the correct bases per their scopes and durations, and any unused time/session component of DO is reflected in remaining_value And taxes are recomputed on post-discount amounts according to T and J for both earned and future amounts And one-time discounts are not re-applied unless explicitly configured And the breakdown shows pre-discount subtotal, discounts by code, taxable base, tax by rate, and totals And rounding and cross-channel consistency are preserved to the cent
Fees, Pause, and Catch-Up Enforcement with Minimum Charge
Given organization-configured fees (e.g., swap_fee, processing_fee) and a policy for pause and catch-up And a minimum_charge_threshold M is configured When a swap introduces a pause or reveals over-consumption (consumed_value > paid_to_date) Then a catch-up amount is created and collected immediately or appended to the next installment per policy, without any installment falling below M And pause defers remaining installments, preserving remaining_value and recalculating due dates without altering earned revenue And fees are itemized and included in totals and are not discounted unless configured And all amounts reconcile and are auditable in UI, API, and receipts
Transparent Breakdown Preview and Audit Log
Given a pending plan swap When the user opens the breakdown preview Then the preview displays: original total, paid_to_date, consumed_value (time or sessions), remaining_value (new principal), discounts by source, taxes by rate, fees, credits/refunds, catch-up amount, new installment schedule (dates/amounts), minimum charge adjustments, and net_today And each line indicates calculation method and rounding applied And the same breakdown is retrievable via API and attached to the receipt after execution And an immutable audit record includes input parameters, calculation_version, gateway transaction IDs, and a breakdown checksum
Gateway Consistency & Idempotent Settlement (Stripe)
Given Stripe as the payment gateway and an idempotency key K for the swap When the swap is executed Then Stripe charges and refunds exactly match the computed net_today and scheduled installments to the cent And Stripe objects carry swap_id and breakdown checksum in metadata And retrying the same request with K produces no duplicate financial operations And any gateway rounding adjustments are recorded and visible in audit and receipts
Deterministic Calculation Engine and Versioning
Given a defined set of inputs and calculation engine version V When the plan swap calculation runs via UI preview, API, or background job Then the outputs are byte-for-byte identical across channels And the response includes calculation_version V and an integrity hash And any rule change increments V and triggers contract tests that must pass for existing swap cases And if parity fails, the swap is blocked with an actionable error detailing mismatches
Pause & Catch-Up Scheduling
"As a coach, I want to temporarily pause a client’s plan and have payments catch up automatically when they return so that I retain the client without losing planned revenue."
Description

Enables optional pause of an active plan with configurable start/end dates, freeze fees, and limits per policy. On resume, automatically recalculates the remaining payment schedule to catch up missed dues without exceeding maximum per‑installment caps or violating final due dates. Maintains enrollment during pause, updates attendance eligibility according to rules, and ensures autopay resumes correctly. Supports pro‑rated extension vs. compressed catch‑up based on studio setting.

Acceptance Criteria
Pause with Configurable Start/End Dates
Given an active payment plan with upcoming installments When an admin creates a pause with a start date S (>= today) and end date E (>= S) Then all installments with due dates between S and E inclusive are marked Paused and are not charged And the plan enrollment status displays as Active (Paused) And the next charge date reflects the first recalculated installment after E per the studio’s catch-up mode And a pause record is created storing S, E, created_by, and timestamp
Freeze Fee Application During Pause
Given a studio policy with a freeze fee configured as flat $F per pause or $W per week and optional max fee $MAX And a tax setting applicable to fees When an admin saves a pause of D days Then the freeze fee equals min($MAX, $F) for flat mode or min($MAX, ceil(D/7) * $W) for weekly mode And the fee is invoiced per policy (immediate or next billing cycle) And applicable taxes are applied And the fee appears on the customer ledger and invoice with a line item labeled Freeze Fee
Pause Policy Limits Enforced
Given policy limits: max_pauses_per_term = N, min_pause_days = m, max_cumulative_pause_days = M And the member has existing pauses recorded in the current term When an admin attempts to create a new pause that would violate any limit Then the system rejects the request and shows specific validation messages for each violated rule And no changes are made to the payment schedule And an audit log entry is recorded with the attempted values and reasons
Resume and Catch-Up: Compressed Mode
Given the plan has compressed catch-up enabled, per-installment cap C, final due date F, and missed dues total MD accumulated during pause When the plan reaches the resume date R Then the system redistributes MD across remaining installments between R and F without any single installment exceeding C And no due date is scheduled after F And additional installments are inserted between R and F if required, honoring the minimum spacing between charges (>= configured days) And the first post-resume charge equals the recalculated amount at the member’s billing time on R And if redistribution is impossible under constraints, recalculation fails with a clear reason and no charges are scheduled
Resume and Catch-Up: Pro-Rated Extension Mode
Given the plan has pro-rated extension enabled with original installment amount A, per-installment cap C, original final due date F, and max_extension_days X When the plan resumes after a pause of D days Then the schedule is extended by min(D, X) days And future installments remain amount A unless additional installments are needed to cover missed dues without exceeding C And no due date is set after F + X And the first post-resume charge runs at the member’s billing time on the resume date And if constraints cannot be met, recalculation fails with a clear reason and no charges are scheduled
Attendance Eligibility During Pause
Given enrollment remains active during pause and attendance policy P ∈ {disallow, limited_k, allow_with_fee} When the member attempts to book a class with a class date between pause start S and end E Then if P = disallow, booking is blocked with the message Your plan is paused and not eligible for attendance during this period And if P = limited_k, bookings are allowed up to k attendances within [S, E]; the (k+1)th attempt is blocked with an over-limit message And if P = allow_with_fee, booking is allowed and a configured drop-in fee is added to the checkout And rosters display a Paused badge on the member during the pause window
Autopay Resumption and Dunning
Given autopay is enabled and a pause with dates S to E exists When the resume date E + 1 day (or configured resume date R) is reached Then no charges are attempted during [S, E] And autopay attempts the next scheduled charge at the account’s billing time on R And no duplicate charge occurs on R if a freeze fee was invoiced immediately at pause creation And on payment failure, dunning rules execute (retry cadence and notifications) while the plan remains resumed (not re-paused)
Eligibility Rules & Guardrails
"As a studio manager, I want guardrails that prevent inappropriate plan changes so that policies are enforced and revenue integrity is maintained."
Description

Defines configurable policies controlling when a plan can be swapped or paused, including minimum time since start, number of attended sessions, outstanding balance limits, discount conflicts, and instructor approval. Prevents swaps that would reduce total obligation below allowed floors, violate promotion terms, or create scheduling conflicts. Includes pre‑flight checks with actionable errors and soft/hard block modes for admins vs. self‑service users.

Acceptance Criteria
Minimum Time Since Start Gate
Given policy.minDaysSinceStart = 14 and a learner’s plan started 15 days ago When the learner requests a swap or pause Then the time-since-start rule passes and the request proceeds to subsequent checks Given policy.minDaysSinceStart = 14 and a learner’s plan started 10 days ago When the learner requests a swap or pause Then the request is blocked with code TIME_SINCE_START and message "Swaps/pauses allowed after 14 days from start" And no plan changes are applied Given policy.minDaysSinceStart = 0 (disabled) When any learner requests a swap or pause Then the time-since-start rule is skipped and does not affect eligibility
Attended Sessions Threshold Enforcement
Given policy.minAttendedSessionsForSwap = 3 and the learner has attended 4 sessions When the learner requests a plan swap Then the attended-sessions rule passes and the request proceeds Given policy.minAttendedSessionsForPause = 2 and the learner has attended 1 session When the learner requests a plan pause Then the request is blocked with code MIN_ATTENDED_SESSIONS and message "Pause available after 2 attended sessions" And no plan changes are applied Given both thresholds are configured and met When the learner requests swap or pause Then the attended-sessions rule contributes no failures to pre-flight results
Outstanding Balance and Delinquency Limits
Given policy.maxOutstandingBalanceForChange = 0 and learner.outstandingBalance = 25.00 When the learner requests a swap or pause Then the request is blocked with code OUTSTANDING_BALANCE and message "Please settle $25.00 to change your plan" And a link to "Pay Balance" is provided in the error action Given policy.maxOutstandingBalanceForChange = 100 and learner.outstandingBalance = 75.00 with no delinquent invoices When the learner requests a swap Then the outstanding balance rule passes and the request proceeds Given policy.blockIfInvoiceOverdue = true and the learner has an invoice overdue by 3 days When the learner requests a pause Then the request is blocked with code INVOICE_DELINQUENT and message "Resolve overdue invoice to pause" And no plan changes are applied
Promotion Terms and Obligation Floor Protection
Given an active promotion with term forbidPlanSwapUntil = 2025-10-01 and today = 2025-09-07 When the learner requests any plan swap Then the request is blocked with code PROMO_TERM_LOCK and message "Plan changes allowed after 2025-10-01 per promotion terms" Given policy.minTotalObligationFloor = 300.00 and the computed post-swap total obligation would be 260.00 When the learner requests the swap Then the request is blocked with code OBLIGATION_FLOOR and message "Total obligation cannot drop below $300.00" And no plan changes are applied Given promotion terms allow swaps and computed post-swap obligation >= floor When the learner requests the swap Then the promotion/floor rule passes and the request proceeds
Instructor Approval Requirement
Given policy.requiresInstructorApprovalFor = [pause, downgrade] and the learner requests a pause When the request is submitted Then an approval request is created for the assigned instructor with status Pending And the learner sees status "Awaiting instructor approval" and cannot change the request outcome directly Given the instructor approves the pending request When approval is recorded Then the pause is applied with the configured effective date And an audit log entry records approver, timestamp, and decision And notifications are sent to the learner and instructor Given the instructor rejects the pending request When rejection is recorded Then the plan remains unchanged And the learner sees reason "Rejected by instructor"
Pre-flight Checks, Actionable Errors, and Block Modes
Given a swap or pause request is initiated When pre-flight validation runs Then the system returns a structured result containing for each failing rule: code, severity (soft|hard), message, remediation, and affected fields Given the actor is a self-service learner and any hard failures exist When they attempt to confirm Then the action is blocked and the UI displays each hard failure with remediation steps Given the actor is a self-service learner and only soft failures exist When they attempt to confirm Then the action is allowed to proceed after explicit acknowledgment of warnings And all warnings remain visible in the confirmation step Given the actor is an admin and hard failures exist When they attempt to confirm Then the action proceeds only if the admin has OverrideHardBlocks permission and supplies an override reason Else the action is blocked Given the actor is an admin and only soft failures exist When they attempt to confirm Then the action proceeds and soft failures are logged as warnings
Scheduling Conflict Prevention on Swap/Pause
Given the target plan’s session schedule overlaps with the learner’s existing bookings by time or resource When the learner requests a plan swap Then the request is blocked with code SCHED_CONFLICT And the response lists each conflicting session with date/time, resource, and conflict type Given the target plan has no conflicts with existing bookings When the learner requests a plan swap Then the scheduling conflict check passes and the request proceeds Given a pause would cancel or skip sessions that are currently waitlisted-to-payment for the learner When the learner requests a pause Then the request is blocked with code WAITLIST_PAYMENT_PENDING and message "Resolve pending payments or remove from waitlist before pausing"
Self-Service Swap UX (Member & Admin)
"As a member, I want an easy way to review and confirm a plan change with a clear preview of costs and dates so that I know exactly what I’m agreeing to."
Description

Delivers streamlined interfaces for members and admins to initiate, preview, and confirm plan swaps or pauses. Provides a real‑time cost/schedule preview, impact on next charge, any fees or credits, policy notices, and required consents. Admins can override within permission scopes; members see only eligible options. UX integrates with booking pages and the account portal, supports mobile, and ensures accessibility (WCAG 2.1 AA).

Acceptance Criteria
Member Swap: Pay-in-X to Pay-in-Full with Real-Time Preview
Given a member with an active Pay-in-X plan and eligible swap options And the member opens Account Portal > Billing & Plans > Swap When the member selects Pay-in-Full Then the preview displays itemized amounts: proration credit, outstanding balance, swap fee (if any), taxes, total due now And the preview displays the next charge date/time in the member’s timezone or “No further charges” if paid in full And the installment schedule (if any) is updated and visible And all monetary values are accurate within $0.01 of server calculation And the preview updates within 700 ms after changing any option or date on a 4G connection And the Confirm Swap button remains disabled until a fresh preview is loaded When the member confirms Then the plan is swapped, invoices are updated, and a success message + email receipt are sent within 60 seconds
Member Pause with Catch-Up Schedule and Enrollment Retention
Given a member has an active installment plan eligible for pause When the member opens the Pause dialog and selects a start date within 0–30 days from today and a duration of 1–8 weeks (per policy) Then the preview shows the pause date range, new next charge date, and extended end date And the preview shows a catch-up schedule with installment counts, amounts, and dates adjusted to finish by the cohort end date if catch-up is enabled And any pause fee and adjusted per-installment amounts are itemized And Confirm Pause is disabled until all required fields are valid and a fresh preview is loaded When the member confirms Then enrollment remains active, upcoming charges are rescheduled with no charges during the pause window, and a confirmation email is sent within 60 seconds And the plan status displays Paused with the resume date in the portal
Policy Notices and Required Consents Gate Confirmation
Given swap/pause actions trigger applicable policies (e.g., fees, non-refundable terms, access impacts) When the dialog renders Then policy notices are displayed with a link to full policy And a required “I agree to the policy” checkbox is present and unchecked by default And the Confirm action is disabled until the checkbox is checked When the user checks the box and confirms Then the system records consent timestamp, user ID, IP address, and policy version And the consent record appears in the audit log and on the receipt/confirmation
Member Sees Only Eligible Swap/Pause Options
Given eligibility rules based on product, account state, and timing windows When rendering the swap/pause options for a member Then only eligible options are displayed And ineligible options are not displayed or selectable via the UI And deep links to ineligible actions return 403 with a user-friendly message And the Confirm action is disabled if eligibility changes before confirmation (e.g., plan becomes ineligible)
Admin Override Within Permission Scope with Audit Trail
Given an admin views a member’s plan And the admin has the Billing Override permission When initiating a swap or pause Then fields for proration credit, swap/pause fee, next charge date, and installment amounts are editable And entering an Override reason (min 10 characters) is required to enable Confirm And the preview recalculates within 700 ms using overridden values When an admin without the permission initiates the flow Then override fields are read-only with a tooltip indicating required permission When an override is confirmed Then an audit record stores admin ID, before/after values, reason, timestamp, and member-notified = true And the member-facing summary reflects any overrides applied
Integrated Entry Points on Booking Pages and Account Portal
Given a booking confirmation page for a newly purchased eligible plan When the user clicks Swap or Pause Plan Then the swap/pause dialog opens pre-populated with the purchased plan context Given the Account Portal > Billing & Plans page When the user clicks Manage Plan Then the same swap/pause dialog opens with the current plan context And deep links include plan ID and return URL parameters And closing/canceling the dialog returns the user to the originating page with no loss of page state
WCAG 2.1 AA Accessible Swap/Pause Dialogs (Desktop & Mobile)
Given a keyboard-only user When navigating the swap/pause dialog Then focus order follows visual order, focus is trapped in the modal, and Escape closes the modal returning focus to the trigger And all interactive controls have accessible names/roles and are operable via keyboard Given a screen reader user When reading the dialog Then headings, labels, and monetary values are announced; errors are associated to inputs; and dynamic preview updates are announced via aria-live polite And color contrast is >= 4.5:1, touch targets are >= 44x44 px, and automated accessibility tests (e.g., axe) report 0 critical violations
Notifications & Consent Records
"As an operator, I want automated, clear notifications and stored consents for plan changes so that clients stay informed and I’m protected in case of charge disputes."
Description

Sends transactional notifications (email/SMS/in‑app) for swap/ pause requests, approvals, schedule changes, upcoming charges, and failures. Includes embedded summaries of proration math and new due dates, with links to detailed receipts. Captures explicit consent and time‑stamped acknowledgments for policy changes and autopay updates, stored with the enrollment for compliance and dispute resolution.

Acceptance Criteria
Swap Request Notification with Proration Summary
Given a learner initiates a plan swap (deposit, pay‑in‑X, or pay‑in‑full) When the swap request is created Then send notifications within 60 seconds to all opted‑in channels (email, in‑app; SMS only if consented) including: proration breakdown (remaining value, credits, discounts/fees, net delta), new due dates/amounts in the learner’s timezone and enrollment currency, and a unique link to the detailed receipt And mask payment details to brand + last4 only And deduplicate so only one notification is sent for the same swap id within 5 minutes And if the primary channel fails, attempt the next available channel and log the failure
Pause Approval and Schedule Update Notifications
Given an instructor approves or denies a pause request When the pause status changes Then notify the learner within 60 seconds with effective pause dates, adjusted installment schedule, and dues changes And if catch‑up rules apply, include catch‑up dates and amounts And if denied, include the denial reason And include a link to the updated schedule and receipt And deduplicate identical status change notifications within 5 minutes And fall back to another consented channel if delivery fails on the first
Upcoming Charge Reminders and Failure Alerts
Given an upcoming installment with autopay active When the due date is 72 hours away Then send a reminder including amount, due date/time (learner’s timezone), and a link to update payment method and view policy summary And if still unpaid at 24 hours before due, send a second reminder (no more than two reminders per installment) Given a payment attempt fails When the provider returns a failure Then notify within 5 minutes with a user‑friendly reason, next retry time, and a link to resolve And if a subsequent retry succeeds, send a confirmation and updated schedule And do not send reminders for fully paid or canceled enrollments
Explicit Consent for Autopay Updates and Policy Changes
Given a swap or pause will change billing schedule, autopay status, or policy terms When the user submits the change Then require explicit consent via checkbox/affirm button with clear statement and link to terms And block submission if consent is not provided And upon consent, create a consent record containing enrollment_id, user_id, action type, terms/policy version, statement hash, financial terms snapshot, timestamp (UTC), user timezone, IP address, user agent, and channel And display a confirmation with a consent reference id
Consent Record Storage, Audit Retrieval, and Export
Given staff views an enrollment’s consent history When opening the Consents tab Then display all consent records with filters by type and date range and provide CSV/JSON export completing within 60 seconds And each record is immutable (updates create a new version) and tamper‑evident via checksum/hash And printable audit output (PDF) includes timestamp, IP, terms version, financial snapshot, and linked event id And records are retained for at least 7 years and exports are scoped to the subject only And a DSAR export can be generated within 5 minutes
Notification Preferences, Channel Consent, and Compliance
Given user channel consents and notification preferences When sending transactional notifications Then honor consent (e.g., SMS only if opted‑in) and include compliant unsubscribe/STOP instructions where required without suppressing critical transactional content And update and log preference changes immediately for subsequent sends And use the user’s locale for template language and formatting And avoid sending via any channel the user has opted out of; use remaining consented channels
Delivery Logging, Status Tracking, and Retries
Given a notification is generated When sending via a provider Then create a NotificationLog with event type, enrollment_id, channel, template version, send timestamp, provider message id, and delivery status updates (queued, sent, delivered, failed, opened when available) And on transient failure, retry up to 3 times with exponential backoff starting at 1 minute; after final failure, create an admin alert (email and in‑app) And if multiple channels are available, follow the user’s priority order; do not send duplicates once delivery is confirmed on a higher‑priority channel And provide a dashboard showing last 30 days send/deliver/fail rates filterable by channel and event type
Accounting Ledger & Audit Events
"As a finance lead, I want a complete audit trail and proper ledger entries for plan swaps so that accounting and reconciliation remain accurate across periods."
Description

Creates immutable ledger entries and audit logs for every swap, pause, credit, fee, and schedule change. Ensures revenue recognition adjustments, tax recalculations, and gateway references are correctly recorded and exportable to accounting systems. Exposes audit events via reports and webhooks for BI and reconciliation, with traceability from original invoice to revised schedule and receipts.

Acceptance Criteria
Immutable Ledger Entry for PlanSwap Change
Given an existing invoice with an active installment plan and a permitted PlanSwap request When the swap is committed Then an immutable ledger entry is created with: entry_id (UUID), event_type "plan_swap", source_invoice_id, customer_id, swap_id, actor_id, created_at (UTC ISO 8601), currency, and line_items And line_items include balanced debits and credits where sum(debits) = sum(credits) And previous_schedule and new_schedule snapshots are persisted in the entry metadata And the entry is read-only after creation (no updates allowed; only compensating entries) And repeated identical requests with the same idempotency_key do not create duplicate entries
Proration & Revenue Recognition Adjustment
Given the consumed service portion through the swap effective_date and the original schedule amounts When a mid-stream swap changes the remaining installments Then the system calculates proration using the plan’s configured policy (time-based or session-based) and posts ledger lines that move amounts between Deferred Revenue and Recognized Revenue accounts accordingly And rounding follows the invoice currency minor unit rules; any rounding difference is posted to a "Rounding Adjustment" account And the adjustment includes references: entry_id, swap_id, effective_date, proration_method, and before/after balances And recognized_revenue_to_date after posting equals the policy-derived amount within 0.01 currency units
Tax Recalculation on Schedule Change
Given a schedule change that affects the taxable base or tax jurisdiction When the change is committed Then tax is recalculated per tax engine settings and tax deltas are posted as separate ledger lines to "Tax Payable" and corresponding revenue accounts And each tax line includes jurisdiction_code, tax_rate, tax_breakdown_id, and links to affected receipt(s) if any And no net tax change is posted when the taxable base is unchanged And negative tax is only produced as a reversal paired with a new positive tax where applicable And totals on the revised invoice equal sum of line items plus tax with 2-decimal precision
Gateway Reference Linking & Traceability
Given any ledger-affecting payment, refund, or authorization related to a swap, pause, credit, or fee When the ledger entry is created Then it stores gateway_provider, transaction_id, and payment_method_fingerprint where applicable And every ledger entry and receipt contains a correlation_id that links back to the original invoice_id and forward to all revisions And a trace report can be generated that enumerates the full chain from original invoice to latest schedule and receipts with no missing links And if the gateway action is pending, the ledger entry records status "pending" and finalization is reflected via a subsequent compensating entry (the original remains unchanged)
Export to Accounting Systems
Given a date range and export format selection (CSV or JSON) When the export is executed Then the file contains all ledger entries and audit events in that range with required fields: entry_id, event_type, created_at, currency, amounts, account_codes, tax_details, gateway_refs, correlation_id And files are paginated deterministically by (created_at, entry_id) with resume tokens for continuation And a SHA-256 checksum and record count are produced; downloaded file checksum and count match the manifest And amounts and signs conform to the configured chart-of-accounts mapping validation rules And export completes within 60 seconds for up to 50,000 entries or returns a job_id for asynchronous retrieval with eventual completion status
Audit Events via Webhooks & Reports
Given an event subscription for audit and ledger topics When a swap, pause, credit, fee, or schedule change occurs Then webhooks ledger.entry.created and audit.plan_change are delivered within 30 seconds with payload fields: event_id (UUID), occurred_at (UTC), entry_id, invoice_id, actor, before_schedule, after_schedule, amounts, gateway_refs, correlation_id, signature And delivery is signed with HMAC-SHA256 using the tenant secret and includes a timestamp header; recipients reject events older than 5 minutes And retries use exponential backoff up to 24 hours with idempotency via event_id to prevent duplicates And the same event appears in the in-app Audit Report within 1 minute and is retained for at least 7 years
Pause and Catch-up Ledgering
Given a plan pause with defined pause_start, pause_end, and a catch_up rule When the pause is activated Then ledger entries freeze deferred revenue accrual during the pause and post a schedule update entry with reason "pause" And upon resume, a catch_up entry posts the additional amounts per the rule without altering recognized revenue to date And the revised schedule reflects shifted due dates and amounts; no missed installment remains unaccounted And all entries include pause_id or catch_up_id and correctly link to the original invoice and subsequent receipts

Role Blueprints

One‑click templates for common ClassTap roles—Owner, Instructor, Substitute, Front Desk, Accountant, and Corporate Portal Admin—preloaded with least‑privilege permissions. Speeds onboarding, keeps access consistent across locations, and reduces costly permission mistakes.

Requirements

Prebuilt Role Blueprint Library
"As an owner, I want to pick a predefined role for a new staff member so that onboarding is fast and secure by default."
Description

Provide out-of-the-box templates for Owner, Instructor, Substitute, Front Desk, Accountant, and Corporate Portal Admin, each mapped to a least‑privilege permission set aligned to ClassTap capabilities (scheduling, payments, invoices, attendee management, reporting). Enable one‑click assignment during user invite, onboarding, and role updates. Ensure templates are immutable system baselines to guarantee consistency, are versioned by platform release, and are backward compatible. Support location scoping and compatible application across single or multiple studios. Enforce idempotent application so reapplying a template produces a consistent permission state.

Acceptance Criteria
Template Library Completeness and Least‑Privilege Mapping
Given an admin opens the Role Blueprint Library When the library loads Then the following system templates are present: Owner, Instructor, Substitute, Front Desk, Accountant, Corporate Portal Admin And each template displays a read‑only permission summary grouped by capability: scheduling, payments, invoices, attendee management, reporting And each template shows a version identifier tied to the current platform release And the allowed actions for each template exactly match the defined baseline for that template with no additional permissions granted
One‑Click Assignment During Invite, Onboarding, and Role Update
Given an admin is inviting a new user When the admin selects a Role Blueprint and clicks Assign Then the user is created with the template’s permissions applied in one action And the assignment flow records the applied template name, version, and location scope Given an admin is onboarding a pending user When the admin selects a Role Blueprint and clicks Assign Then the permissions are applied in one action with a confirmation summary Given an admin edits an existing user’s roles When the admin selects a Role Blueprint and clicks Apply Then the user’s permissions update to match the template baseline without requiring manual permission toggles
Immutable System Templates
Given a system Role Blueprint template is selected When an admin attempts to edit or delete the template Then edit and delete actions are unavailable and the template appears as read‑only And the UI indicates the template is a system baseline And any permission changes must be done outside the system template (templates themselves remain immutable)
Versioning and Backward Compatibility
Given templates are versioned by platform release When a new platform release publishes template version N+1 Then existing users retain the permissions of the previously applied version without automatic changes And admins can explicitly upgrade a user’s template application to version N+1 with a preview of differences And users on older versions continue to function without errors And reapplying the same older version restores its baseline permissions for that user
Location Scoping for Single and Multi‑Studio Tenants
Given a tenant with multiple locations When an admin assigns a Role Blueprint and selects specific locations Then the user’s effective permissions are limited to the selected locations only And actions outside the selected locations are blocked Given a tenant with a single location When an admin assigns a Role Blueprint Then the location scope defaults to that location and cannot be set to others
Idempotent Template Application
Given a user has Role Blueprint X version V applied with locations L When the admin reapplies Role Blueprint X version V with the same locations L Then the user’s permission state after reapplication exactly matches the template baseline for X version V scoped to L And any drift from the baseline is corrected And no duplicate, residual, or conflicting permissions remain after reapplication And repeating the operation yields the same permission state each time
Bulk Assignment and Location Scoping
"As a multi‑location admin, I want to assign the Front Desk role across selected studios in one action so that access stays consistent and scalable."
Description

Deliver UI and API to assign or update role blueprints for multiple users at once, with options to scope access to specific locations, resources, and calendars. Support setting a default blueprint for new invites per location, and provide safeguards to prevent unintended escalation when users belong to multiple locations. Handle conflict resolution rules when merging existing custom permissions with a blueprint application. Provide progress feedback, partial‑failure handling, and a summary of changes applied.

Acceptance Criteria
UI Bulk Assignment with Location/Resource/Calendar Scoping
Given I am an Owner on the Role Blueprints page with Manage Roles permission and have selected 25 users across 3 locations And I choose the "Instructor" blueprint And I scope access to Location A (resources: R1,R2; calendars: C1), Location B (resources: none; calendars: none), and Location C (resources: R3; calendars: C2,C3) When I click Apply and confirm Then a confirmation modal displays totals (users=25, locations=3, resources=3, calendars=3) and a diff of permissions to be added/removed And a background job starts and a progress indicator appears within 2 seconds And upon completion, 25 of 25 users are updated within 120 seconds And each user’s effective permissions match the "Instructor" blueprint constrained only to the specified scopes And no user gains access to locations, resources, or calendars not listed And an audit log entry is recorded per user with before/after permissions and scopes
API Bulk Assignment with Idempotency and Job Tracking
Given a POST /v1/role-blueprints/apply request with valid auth, 500 user_ids, a blueprint_id, per-location scopes, and header Idempotency-Key=abc123 When the request is submitted Then the API returns 202 Accepted with job_id and estimated_counts (total=500) within 1 second And a repeat POST within 24 hours with the same Idempotency-Key returns 200 with the original job_id and no duplicate work is performed And GET /v1/jobs/{job_id} returns progress {completed, failed, pending} and per-user results on completion And rate limiting enforces at most 10 bulk-apply requests per minute per org, returning 429 on exceed And invalid user_ids are reported in errors[] with codes and messages while valid users proceed unaffected And per-user operations are atomic; on error for a user, no partial permission changes are applied to that user
Default Blueprint per Location for New Invites
Given Location A has default blueprint "Front Desk" with default scopes (Location A: all resources; calendar X) When I invite a new user assigned to Location A via UI or API without override Then the invite is created with the "Front Desk" blueprint and the configured default scopes And changing the default blueprint later does not modify existing users or pending invites And I can override the default during invite; the override is applied and audited And disabling the default for Location A stops auto-assignment for subsequent invites And an audit event records who set or changed the default and when
Safeguard Against Cross-Location Permission Escalation
Given a user currently has the "Front Desk" blueprint at Location A And I bulk-assign the "Owner" blueprint scoped to Location B When the system computes effective permissions across locations Then a warning modal flags net new high-risk capabilities and shows a before/after diff per location And the Apply button is disabled until I type CONFIRM to acknowledge elevation And if org policy "Block High-Risk Elevation" is enabled, the operation is blocked with error code POLICY_BLOCK and no changes are applied And if I enable the "Safe Merge" option, the assignment proceeds but strips capabilities exceeding the org’s maximum allowed for that user category, preventing unintended escalation And all outcomes are recorded in the audit log with rationale
Conflict Resolution When Merging with Existing Custom Permissions
Given a user has custom permissions {Edit Invoices, Export Reports} at Location A and is not currently on a blueprint When I apply the "Instructor" blueprint with merge_mode=replace Then all custom permissions not in the blueprint are removed and replaced by the blueprint’s least-privilege set When I apply with merge_mode=merge_restrictive Then only permissions present in both the user’s custom set and the blueprint are retained; others are removed When I apply with merge_mode=merge_permissive Then permissions become the union of custom and blueprint, but any permissions outside allowed_merge_whitelist are excluded and reported And before applying, a preview shows per-user permission deltas; after applying, a summary lists kept, added, and removed permissions per user
Progress Feedback, Partial Failure Handling, and Summary Export
Given a bulk assignment job for 1,000 users is running When 50 users fail due to validation errors and 950 succeed Then the UI shows real-time progress with succeeded=950, failed=50, remaining=0, and elapsed time And the job completes with overall status=Partial Success And I can retry only the 50 failed users via a single click or POST /v1/jobs/{job_id}/retry?failed_only=true And I can download CSV and JSON summaries containing per-user status, error codes, before/after permissions and scopes, actor, timestamps, and correlation/job IDs And no succeeded user is rolled back due to other users’ failures
Custom Blueprint Editor and Versioning
"As an owner, I want to tailor the Instructor role to my studio’s policies and roll it out safely so that I preserve least privilege while meeting local needs."
Description

Allow admins to clone system blueprints or build from scratch, adjust permissions via granular toggles, and save as reusable custom blueprints. Maintain semantic versioning for each custom blueprint with diff views, change notes, and rollback to prior versions. Propagate updates to all assigned users with optional staged rollout windows and effective‑date scheduling. Validate configurations against least‑privilege guidelines and flag risky permissions with warnings. Support draft, published, and deprecated states and enforce migration rules when deprecating.

Acceptance Criteria
Create or Clone Custom Blueprint and Save as Draft v1.0.0
Given I am an Org Admin with permission to manage blueprints When I clone the system blueprint "Instructor" and adjust multiple granular permission toggles And I provide a unique name 3–50 characters using letters, numbers, spaces, hyphens, or underscores And I click Save Then a custom blueprint is created in state Draft with version 1.0.0 And the saved permissions exactly match the toggles selected And an audit record is stored with actor, timestamp (UTC), source blueprint id, and change summary Given I create a new blueprint from scratch When I click Save without enabling any permissions Then the blueprint is created with all permissions disabled, state Draft, version 1.0.0
Edit Blueprint with Semantic Versioning and Mandatory Change Notes
Given a custom blueprint is in state Published at version v1.2.0 When I modify its permissions and click Save Changes Then I am prompted to select the SemVer increment (Major/Minor/Patch) with Minor preselected And I must enter a change note of at least 10 characters And a new version is created with valid SemVer format X.Y.Z reflecting the selected increment And the previous version remains immutable and available for assignment history and diff And the system blocks saving if change notes are missing or the version selection is invalid
Compare Versions with Permission-Level Diff View
Given a custom blueprint has versions v1.1.0 and v1.2.0 When I open the Diff view between those two versions Then I see permission changes categorized with counts for Added, Removed, and Unchanged And metadata changes (e.g., name, description) and the change notes are displayed And the diff renders within 2 seconds for blueprints with up to 500 permissions And I can switch the base/target versions without leaving the view
Roll Back to a Prior Version and Republish
Given a Published blueprint with versions up to v2.0.0 has active assignees When I choose Rollback to version v1.2.0 and provide a reason Then the system creates a new version v2.0.1 whose content exactly matches v1.2.0 And v2.0.1 becomes the current Published version And an audit entry records rollback source version, target version, actor, timestamp, and reason And assigned users are updated according to selected propagation options (immediate, scheduled, or staged)
Publish with Scheduled Effective Date and Staged Rollout
Given a blueprint version v1.3.0 is ready to publish When I configure a staged rollout with at least two waves and specify effective date/time per wave in the org timezone (5-minute granularity) And I click Publish Then no users receive the update before their wave’s effective date/time And users in each wave receive the update within 5 minutes of the scheduled time And a rollout status view shows per-wave completion and failures And I can pause, resume, or cancel the rollout before the final wave starts And canceling reverts any updated users to the previously assigned version
Validate Least-Privilege Compliance and Risk Warnings
Given the blueprint contains permissions flagged as risky by least-privilege rules When I attempt to Save (Draft) or Publish Then the system displays per-permission risk severity (Low/Medium/High) and an overall risk level And saving as Draft is allowed with warnings And publishing is blocked for High risk unless I provide a justification of at least 20 characters And all warnings and overrides are recorded in the audit log
Manage Lifecycle: Draft, Published, Deprecated with Migration Rules
Given a blueprint is currently Published and assigned to users When I mark it as Deprecated Then I must select a replacement blueprint/version and an end-of-life date within 180 days And new assignments to the deprecated blueprint are blocked immediately And existing assignees are scheduled for migration to the replacement per selected timing And after the end-of-life date, any remaining assignees are auto-migrated And deprecated blueprints are read-only except for metadata and migration settings
Impact Preview and Safe Apply
"As an admin, I want to preview the exact permission changes before applying a blueprint so that I avoid accidental over‑permissioning."
Description

Before saving or applying a blueprint, present a preview of net changes (added, removed, or scope‑reduced permissions) with risk indicators for sensitive actions (payouts, refunds, schedule overrides). Simulate effects across selected locations and users, estimate the number of impacted accounts, and show any conflicts with existing custom permissions. Require explicit confirmation for high‑risk changes and support dry‑run mode via API. Apply changes via background jobs with progress tracking and provide a post‑apply success/failure report and undo option where possible.

Acceptance Criteria
Preview: Net Permission Changes
Given an admin selects a role blueprint, target users, and one or more locations When the admin clicks Preview Changes Then the preview displays distinct lists for Added, Removed, and Scope-Reduced permissions, each with per-permission entries and total counts And the preview displays a total impacted accounts estimate And the preview can be filtered by change type and permission name And the preview renders within 3 seconds for up to 5,000 targeted accounts
Risk Indicators and Explicit Confirmation
Given the preview contains sensitive permissions (payouts, refunds, schedule overrides) When the admin views the preview Then each sensitive permission change is marked with a Risk badge indicating sensitivity level And the Apply button remains disabled until the admin checks "I understand the risks" and types APPLY to confirm And an audit log entry records the confirmation action, including actor, timestamp, and risk items acknowledged
Conflict Detection with Existing Custom Permissions
Given any targeted user has custom permissions that differ from the selected blueprint When the preview is generated Then a Conflicts panel lists each conflict with user, location, permission, and conflict type (extra, missing, or scope-different) And the admin must choose a resolution per conflict (Override to blueprint or Retain custom) before Apply is enabled And the preview shows total conflicted users and total conflicts
Simulation Across Locations and Impact Estimation Accuracy
Given multiple locations are selected When the admin runs a preview Then the system shows impacted account counts broken down by location and in total And the post-apply report shows actual impacted accounts within ±1% of the preview estimate (or ±5 accounts, whichever is greater)
API Dry-Run Mode
Given an authorized client calls POST /role-blueprints/apply with dryRun=true, a valid blueprintId, and target filters When the request is processed Then the response status is 200 with a body containing counts for Added, Removed, Scope-Reduced, conflicts, and impacted accounts by location And no permission changes are persisted and no background job is created And an audit log entry is written labeled Dry-Run with the request parameters and summary
Background Apply and Progress Tracking
Given the admin clicks Apply on a valid preview When the apply process starts Then a background job is created with a unique jobId and initial status queued And the UI shows a progress indicator with percent complete, processed/total accounts, current phase, and ETA, refreshing at least every 2 seconds And a status API GET /jobs/{jobId} returns job state (queued, running, succeeded, failed, cancelled), percent, and counts And transient failures are retried up to 3 times per account before being marked failed And navigating away from the page does not cancel the job, and progress resumes when returning
Post-Apply Report and Undo
Given the background job completes When the admin opens the Results Then a downloadable report (CSV and JSON) is available listing each account and permission change with outcome (Success/Failed) and error reason where applicable And the report includes totals for Added, Removed, Scope-Reduced, conflicts resolved by choice, and failures And an Undo option is offered for reversible changes for 7 days, initiating a rollback job that restores the prior permission snapshot And after Undo completes, the effective permissions for targeted accounts match their pre-apply state; non-reversible items are labeled Not Undoable
Audit Trail and Compliance Export
"As an owner, I want a complete audit log of role and permission changes so that I can prove compliance and investigate issues."
Description

Capture immutable audit events for blueprint lifecycle (create, edit, publish, deprecate, delete), assignments, removals, and scope changes, including actor, timestamp, target users, and before/after permission diffs. Provide filterable views, retention settings, and export to CSV/JSON. Offer webhooks for real‑time notifications to external compliance tooling and a signed report for attestation during audits. Ensure time‑synchronized logging and tamper‑evident storage to meet compliance expectations.

Acceptance Criteria
Immutable Audit Log for Blueprint Lifecycle
Given an authenticated user with permission to manage Role Blueprints performs a lifecycle action (create, edit, publish, deprecate, delete) When the action is committed Then an audit event is appended within 1 second containing: event_id, event_type ∈ {create, edit, publish, deprecate, delete}, actor_id, actor_role, blueprint_id, timestamp (UTC ISO 8601 with ms), before_permissions, after_permissions, request_id, record_hash, prev_hash And the event is retrievable via Audit Log API/UI within 2 seconds of commit And subsequent attempts to modify or delete this event via any supported API/UI are rejected with 403 and the stored event remains byte-for-byte unchanged And record_hash and prev_hash correctly validate the append-only chain for the event when verified over the preceding 1,000 events
Audit Events for Assignments and Scope Changes
Given a batch operation assigns a Role Blueprint to N users, removes it from M users, or changes its scope (e.g., location or department) When the operation completes Then a single audit event per operation is recorded with: event_type ∈ {assign, remove, scope_change}, actor_id, blueprint_id, timestamp (UTC ISO 8601 with ms), target_user_ids (up to 1,000 listed), targets_count, overflow_count (if targets_count > 1,000), before_scope, after_scope, effective_permission_delta And the event is queryable by target_user_id via API filter and included in filtered UI results And events from the same request_id are ordered deterministically by timestamp then event_id
Filterable Audit Log View
Given filters for date_range (start,end in UTC), event_type, actor_id, blueprint_id, target_user_id, and scope When the filters are applied in the Audit Log UI/API Then only matching events are returned, sorted by timestamp DESC by default, with stable cursor-based pagination And for up to 10,000 matching events, first page p50 latency ≤ 2s and p95 ≤ 3s; subsequent pages p50 ≤ 1s and p95 ≤ 2s And clearing or changing a filter updates the results and total_count consistently within 1 second And exported results (when triggered) exactly match the current filtered set and ordering
CSV and JSON Export with Pagination
Given a user requests an export for a specified date_range and filters, choosing CSV or JSON format When the export job runs Then the export includes all matching events in strict timestamp ASC order and is chunked for data sets > 50,000 records with a manifest of parts And CSV files are UTF-8, RFC4180-compliant with a header row and fields: event_id, event_type, actor_id, actor_role, blueprint_id, timestamp, target_user_ids, targets_count, before_permissions, after_permissions, before_scope, after_scope, request_id, record_hash, prev_hash And JSON exports are line-delimited JSON (NDJSON), one event object per line with the same fields And exports up to 100,000 events complete within 2 minutes end-to-end; larger exports proceed asynchronously and provide downloadable parts as they complete And a checksum (SHA-256) is provided per file and matches the downloaded content
Retention Policy and Tamper Evidence
Given an organization retention_days setting between 30 and 3650 (default 3650) When retention_days is updated by an authorized admin Then the new setting is persisted, audited, and displayed in settings with the effective date/time And events older than retention_days are purged by a daily job, and a summarizing purge audit event records the purged_count and cutoff_timestamp And an integrity verification endpoint returns chain_ok=true for the audit log over a requested date range; any alteration of a stored record causes chain_ok=false with first_bad_event_id reported And all audit event timestamps are recorded in UTC ISO 8601 with ms and differ from authoritative server time by ≤ 1 second at write time
Real-Time Compliance Webhooks
Given a webhook endpoint is configured with a secret and subscribed to audit event types When qualifying audit events occur Then a POST is delivered within 5 seconds of commit with JSON payload of the event and headers X-CT-Signature (HMAC-SHA256), X-CT-Idempotency-Key, X-CT-Timestamp And non-2xx responses trigger exponential backoff retries for up to 24 hours; events are delivered at-least-once and the idempotency key remains stable across retries And delivery status (success/failure, last_attempt_at, attempt_count) is visible in the UI/API per endpoint and per event And rotating the webhook secret results in subsequent deliveries being signed with the new secret and verified successfully
Signed Audit Attestation Report
Given an authorized user requests an attestation report for a selected date_range and filters When the report is generated Then the system produces a PDF (human-readable) and a JSON manifest containing org_id, date_range, generated_at (UTC ISO 8601 with ms), total_events, counts_by_event_type, export_manifest (file names and SHA-256 checksums), integrity_verification_result, and a detached JWS signature over the manifest And the JWS verifies successfully against the published ClassTap public key, and the checksums match the downloadable export files And for up to 100,000 events in range, the report is available within 2 minutes; larger ranges run asynchronously with progress status
Org-Level Inheritance with Local Overrides
"As a corporate admin, I want to set standard roles that locations can lightly customize so that access remains consistent across the brand."
Description

Enable corporate or franchise organizations to define org‑level standard blueprints that locations inherit. Allow location admins to apply permitted local overrides within guardrails set by corporate (e.g., cannot grant payout permissions). Provide clear inheritance visualization, per‑permission lock controls, and conflict resolution rules. Support updates at the org level that roll down automatically with optional review windows for locations, ensuring brand‑wide consistency without blocking local needs.

Acceptance Criteria
Org Blueprint Inheritance on Location Creation
Given an organization has a published standard blueprint named "Front Desk" And the blueprint contains a defined set of permissions with some marked as locked by org When a new location is created and associated to the organization Then the "Front Desk" blueprint is automatically available at the location And the permission set at the location exactly matches the org blueprint values And org-locked permissions are displayed as locked at the location and cannot be changed And an audit entry is recorded for the inheritance event with timestamp and actor "system"
Guardrails Enforcement for Local Overrides
Given corporate has set guardrails that prohibit enabling the permission "Manage Payouts" And a location admin opens an inherited blueprint that includes "Manage Payouts" When the location admin attempts to enable or modify "Manage Payouts" Then the control is disabled and visually marked with a lock icon And a tooltip states "Managed at Org: cannot be modified locally" And saving the blueprint with this change is blocked with error code RB-403 and message "Permission locked by org" And the blocked attempt is logged with user, permission, timestamp, and reason
Org-Level Updates with Location Review Window
Given an org updates the "Instructor" blueprint by enabling "View Attendee Contact" And a 7-day review window is configured for blueprint roll-downs When the org publishes the update Then each location receives a pending update with a visible diff highlighting added/removed/changed permissions And the location can Accept or Defer the update within 7 days And if no action is taken by the end of day 7, the update auto-applies And the acceptance, deferral, or auto-apply outcome is recorded per location with timestamp and actor And until applied, the location’s live permissions remain unchanged
Per-Permission Lock Controls and Visualization
Given a location admin views an inherited blueprint Then each permission displays one of three states: Inherited-Locked, Inherited-Overridable, or Locally-Overridden And lock icons appear for Inherited-Locked with tooltip "Locked by Org" And an "Overrides Only" filter toggles the list to show only Locally-Overridden permissions And attempting to edit an Inherited-Locked permission is not possible (no interactive control focusable) And screen-reader labels announce the permission name and state (e.g., "Check-In: Locked by Org")
Conflict Resolution Precedence and Auto-Apply Behavior
Given the org sets permission "Issue Refunds" to Locked-On When a location attempts to disable "Issue Refunds" on the inherited blueprint Then the action is blocked and the permission remains enabled Given the org sets permission "Manage Payouts" to Locked-Off When a location attempts to enable "Manage Payouts" Then the action is blocked and the permission remains disabled Given a permission previously marked Overridable is changed by org to Locked-On or Locked-Off When the update is published with a review window Then all locations receive the update and any local overrides for that permission are removed upon apply (accept or auto-apply) And the final value equals the org lock value
Roll-Down Propagation and Consistency Across UI and API
Given the org publishes a blueprint update with no review window When the update is applied Then 95% of locations reflect the new permission values in the Admin UI within 5 minutes and 100% within 15 minutes And the GET /v1/roles/blueprints API returns the updated values for an applied location within 1 minute of the UI reflecting the change And a reconciliation job identifies and corrects any drift so that API and UI are consistent for all applied locations within 15 minutes And propagation status is visible per location (e.g., Pending, Applying, Applied)
Access Review and Recertification
"As an owner, I want periodic prompts to review and confirm who has which roles so that we reduce risk from outdated or unnecessary access."
Description

Implement periodic access reviews on a configurable cadence with reminders to owners or corporate admins to re‑certify user‑role assignments. Provide a streamlined review UI showing current blueprint, last change date, and activity signals (e.g., last login, bookings handled). Support bulk revoke, role change recommendations, and generation of attestation reports. Auto‑flag inactive or role‑drifted accounts for action and support policy‑based auto‑disable after non‑response.

Acceptance Criteria
Configure Quarterly Access Review Cadence With Reminders
Given I am an Owner or Corporate Portal Admin with permission to manage access reviews When I set the cadence to "Quarterly", select tenant timezone "America/New_York", set due day to the 1st, and enable reminders at -14 and -3 days Then the system calculates the next due date on the upcoming 1st, stores the schedule, and displays it in Settings Given a schedule exists When the due date arrives Then initial notifications are sent via email and in-app to designated reviewers with a review link, and reminder notifications fire at -14 and -3 days Given I change the cadence to "Monthly" When I save Then the schedule updates without creating duplicate review cycles and the next due date is recalculated per the new cadence Given no timezone is set When I save the cadence Then the tenant primary location timezone is used by default
Review UI Displays Blueprint, Last Change, and Activity Signals
Given I open the Access Review for a user When the review card is rendered Then it shows current Role Blueprint name, last role change date/time, last login date/time, and activity summary (bookings handled in last 90 days), or "No activity" if zero Given 500 users are in scope When I load the review list Then the initial view renders within 2 seconds at the 95th percentile Given I have location-scoped permissions When I view the review list Then I only see users within my permitted locations
Bulk Revoke and Apply Role Recommendations
Given a set of users are flagged as Inactive or Role Drifted When I select them and click "Apply Recommendations" Then the system proposes per-user actions (revoke, change to Recommended Blueprint) and requires confirmation Given I confirm the bulk action When execution completes Then successful changes are applied, affected users are notified, booking assignments are handled per reassignment policy, and an audit record is created per user Given partial failures occur When execution completes Then I see a summary with success/failure counts and retriable errors Given I am reviewing my own account When I attempt to revoke my own access Then the action is blocked with an explanation
Auto-Flag Inactive and Role-Drifted Accounts
Given inactivity threshold = 45 days and activity baseline = 0 bookings in last 60 days When a user exceeds the threshold, has never logged in, or meets the zero-activity baseline Then the user is flagged "Inactive" for review Given a user's current permissions differ from their assigned Role Blueprint When a drift is detected Then the user is flagged "Role Drifted" with a permissions diff summary Given nightly evaluation is enabled When the job runs Then flags and recommendations are updated and visible in filters; real-time updates occur within 5 minutes of a permission change
Policy-Based Auto-Disable After Review Non-Response
Given auto-disable policy is enabled with a 7-day grace period after the review due date When an access review for a user remains incomplete after 7 days Then the user account is set to "Suspended" (login blocked; cannot be assigned to new classes) and Owners/Corporate Admins are notified Given a suspended user is approved during recertification When the reviewer submits the approval Then the account is re-enabled immediately and the suspension reason is preserved in audit logs Given excluded roles include Owner When the policy job runs Then Owner accounts are never auto-disabled
Generate and Export Attestation Report
Given a review cycle is completed When I click "Generate Attestation Report" Then the report contains tenant details, cycle dates, reviewer identity, decision per user (approve/change/revoke), timestamps, rationale comments, and before/after roles Given I need to export the report When I choose CSV or PDF Then the file downloads successfully with a tamper-evident checksum and the export event is logged Given a retention policy of 7 years When the report is stored Then it remains accessible to Owners and Corporate Admins for 7 years
Audit Logging and Traceability For Access Reviews
Given any access review configuration or action occurs (schedule change, decision, bulk apply, suspend/enable, report export) When the event is committed Then an immutable audit record is written with actor, timestamp, action type, old/new values, scope, and IP/device metadata Given I open the Audit Log When I filter by "Access Review" Then I can query by user, reviewer, date range, action type, and export results to CSV Given a multi-location tenant When I view audit entries Then each entry includes location context and my visibility is restricted to my permitted locations

ScopeGuard

Granular scoping that limits what each role can see and do by location, room, class type, client segment, and financial areas (e.g., view‑only payouts, no exports). Enforces need‑to‑know access, protects client PII, and fits multi‑instructor, multi‑site workflows.

Requirements

Scoped Role Access Matrix
"As an owner, I want to assign precise permissions by role and scope so that each team member only sees and does what they need."
Description

Define a role-permission matrix that supports granular scopes across location, room, class type, client segment, and financial domains. Enforce a default‑deny model with capability flags (e.g., view, edit, create, delete, export) that can be independently enabled per scope. Integrate enforcement in both UI and API layers, including background jobs and webhooks, to prevent privilege bypass. Provide an admin UI to assign roles and attach one or more scopes per user, with inheritance for multi-site organizations. Ensure backward compatibility by offering a migration path from existing roles to scoped roles. Expected outcome: consistent least‑privilege access that fits multi‑instructor, multi‑site workflows without blocking legitimate tasks.

Acceptance Criteria
Default-Deny Access Without Assigned Scopes
Given a user has a role assigned but zero scopes When the user attempts any of [view, edit, create, delete, export] on any resource via UI or API Then the API responds 403 Forbidden with no resource data returned And the UI hides or disables the corresponding controls and displays a generic permission error And no background job or webhook is executed as a result of the denied action
Capability Flags Enforced Per Scope and Resource Dimensions
Given user U has role Instructor with a scope (Location=NYC Downtown, Room=Studio A, ClassType=Yoga, ClientSegment=Adults) and capabilities [view, create] When U creates a Yoga class in NYC Downtown, Room Studio A for the Adults segment Then the operation succeeds with 201 Created and the class is visible to U And the same create outside any scoped dimension (e.g., Room=Studio B or ClientSegment=Teens) is denied with 403 And attempts to edit, delete, or export within the scope are denied with 403 And viewing within the scope returns data; viewing outside the scope returns 403
Multiple Scopes Aggregate Without Privilege Escalation
Given user U has two scopes: S1 (Location=NYC Downtown, Room=Studio A, ClassType=Yoga, capabilities [view, edit]) and S2 (Location=NYC Downtown, Room=Studio B, ClassType=Pilates, capabilities [view]) When U attempts to edit a Pilates class in NYC Downtown, Room=Studio B Then the action is denied with 403 And U can edit Yoga classes only in NYC Downtown, Room=Studio A And U can view Yoga in Studio A and Pilates in Studio B And any attempt at NYC Uptown (no scope) is denied with 403
Consistent Enforcement Across UI, API, Background Jobs, and Webhooks
Given user U initiates an action that executes asynchronously (e.g., bulk cancel classes, mass email clients, bulk roster edit) or triggers webhooks When the target selection includes resources both inside and outside U's scopes/capabilities Then the system processes only in-scope resources and reports out-of-scope items as skipped due to permissions And any API request that references out-of-scope resource IDs returns 403 And emitted webhooks and job side effects include only in-scope resources; client PII for out-of-scope segments is not included in payloads
Financial Domains: View-Only Payouts, No Export
Given a role grants financial:payouts=view and financial:export=disabled within specified locations When the user views payout summaries and payout details within their scoped locations Then the views succeed and show amounts and statuses, but export/download controls are not rendered And any attempt to access payout export endpoints returns 403 and generates no file And direct URL access to CSV/Excel exports returns 403
Admin Assigns Scoped Roles with Organization-Level Inheritance
Given an Org Admin opens the Roles & Access admin UI When the admin assigns role Manager to user U and attaches scopes S1 (Organization-level: ClassType=Pilates, capabilities [view, edit]) and S2 (Location=Oakland, Room=Studio 2, capabilities [view]) Then saving persists both scopes successfully And S1 inherits to all current and future locations unless explicitly overridden, while S2 narrows access at Oakland Studio 2 And U can edit Pilates across all locations except where a more restrictive scope denies that action And updated permissions propagate to UI and API within 60 seconds
Legacy Role Migration to Scoped Roles
Given an account uses legacy roles without scopes When the migration tool runs in dry-run mode Then it produces a report mapping each user from legacy roles to proposed scoped roles and scopes, with detected capability differences When the admin confirms and executes the migration Then users receive scoped roles that preserve least-privilege access equivalent to prior legitimate tasks and newly deny out-of-scope actions And the migration is idempotent (re-running does not duplicate scopes) and requires no downtime
Row‑Level Data Filtering by Scope
"As a coordinator, I want my views automatically limited to my locations and class types so that I don’t see or change data outside my remit."
Description

Automatically filter all list, search, calendar, and detail views so users only see entities (classes, bookings, clients, rooms, payouts) within their assigned scopes. Apply server‑side row‑level filtering with query builders and policy middleware; mirror filters in the client to minimize confusing UI elements. Ensure cross‑feature coverage: scheduler, waitlist, double‑booking checks, notifications, and reports. Handle edge cases like multi‑scope users, cross‑site bookings, and shared rooms with deterministic rules and clear UI labels. Expected outcome: scoped visibility that reduces noise, protects PII, and prevents accidental cross‑site edits.

Acceptance Criteria
Server-Side Row-Level Filtering Across All Views
Given a user has scopes Location=A and ClassTypes=[Yoga] When they request any list, search, calendar, or detail endpoint for classes, bookings, clients, rooms, or payouts Then only rows matching the user’s assigned scopes are returned And when requesting an out-of-scope entity by ID Then the API responds with 404 or 403 without revealing entity existence And client-side filters cannot expand results beyond the enforced server-side scope
Client UI Mirrors Scope and Reduces Noise
Given a scoped user opens any list or calendar view When the page renders Then a non-removable scope badge displays the active scopes (e.g., Location A; Types: Yoga) And out-of-scope options are not available in pickers, searches, or quick-create menus And pasting a URL to an out-of-scope entity shows an access-restricted message with no PII
Deterministic Rules for Multi-Scope Users and Shared Rooms
Given a user has multiple scopes (Locations A,B; ClassTypes Yoga,Pilates; specific Rooms) When retrieving entities Then visibility equals the union per dimension with entity-specific AND semantics (e.g., class visible if location∈{A,B} AND class_type∈{Yoga,Pilates}) And rooms assigned directly are visible even if used by out-of-scope classes, but booking/client details are redacted unless in-scope And the entity detail header shows the scope that granted visibility (e.g., "Visible via: Location A")
Double-Booking Checks Honor Scope Without PII Leakage
Given a scoped user creates or edits a booking When a time/room conflict exists with an out-of-scope booking Then the action is blocked And the message reads "Room unavailable (restricted)" without client names or class details And API responses return 409 with a non-PII conflict descriptor When the conflict is in-scope Then the detailed conflict information is shown
Scoped Waitlist-to-Payment Flow
Given a scoped user promotes a waitlisted client to a paid booking When selecting classes, clients, and payment options Then only in-scope classes and clients are selectable And notifications are sent only to in-scope recipients And attempts to reference out-of-scope entities are rejected with 403 or validation errors without revealing PII
Scoped Financials: View-Only Payouts and No Exports
Given a role limited to view-only payouts and no exports for Location A When viewing payouts lists or details Then only in-scope payouts are shown and edit/export controls are absent When calling export or edit endpoints Then the request is denied with 403 and no file is generated or state changed And direct access to out-of-scope payout IDs returns 404 or 403
Reports and Notifications Contain Only Scoped Data
Given a scoped user generates reports or configures notifications When selecting inputs (locations, class types, date ranges) Then only in-scope dimensions are available And generated outputs include only scoped rows and totals equal the sum of in-scope data And downloaded files contain no out-of-scope rows And scheduled reports/notifications cannot target out-of-scope audiences (validation fails)
Granular Financial Controls (View‑Only, No Export, Action Gates)
"As a studio manager, I want staff to see payout summaries but not export or refund so that financial data stays controlled while operations continue smoothly."
Description

Introduce fine‑grained permissions for financial areas: view payouts vs. view balances, create/edit invoices, process refunds, issue credits, capture payments, and export financial data. Support view‑only modes and explicit export bans per role/scope. Add action gates in UI/API that require elevated permission or owner approval for sensitive operations (e.g., refunds over a threshold). Surface audit banners and disabled states explaining restrictions. Expected outcome: minimized financial risk and clearer separation of duties without blocking day‑to‑day scheduling work.

Acceptance Criteria
View-Only Payouts Permission
Given a user role with financial.payouts.view=true, financial.balances.view=false, and financial.exports=false When the user navigates to Finance > Payouts Then payout lists and payout details are visible without edit/create/delete controls And balances widgets/sections are hidden or replaced with an “insufficient permission” message And export buttons/menus are not rendered And any POST/PUT/DELETE to /api/payouts returns 403 FINANCIAL_ACTION_FORBIDDEN and is audit-logged with userId, roleId, scope, endpoint, and timestamp
Export Ban Enforced (UI/API/Audit)
Given a role where financial.exports=false across all scopes When the user visits Invoices, Payouts, or Transactions pages Then export affordances (CSV/XLSX/PDF) are absent or disabled with tooltip “Export disabled by role” When the user invokes GET/POST /exports or requests format=csv|xlsx on financial endpoints Then the response is 403 with code=EXPORTS_DISABLED, correlationId, and helpUrl And an on-page audit banner states “Exports disabled for your role” with a Request Access link And an audit entry type=EXPORT_BLOCKED, reason=ROLE_POLICY is recorded
Refunds Over Threshold Require Approval Gate
Given org.refundApprovalThreshold=100.00 and user role refunds_up_to=100.00 When the user initiates a refund for $120 on invoice INV-123 Then the refund is blocked behind an approval gate and an approvalRequest is created with approverRole=Owner and status=Pending And the UI shows a modal explaining the threshold and enables submitting the approval request; no funds are moved And upon owner approval within 24h the refund posts and the requester is notified; if rejected or expired, no refund occurs And POST /refunds returns 202 PENDING with approvalRequestId when above threshold, and 201 CREATED when within user limit
Scoped Financial Access by Location and Class Type
Given the user is scoped to location=Studio A and classType=Yoga with invoice.view=true When the user searches invoices or transactions Then only records with location=Studio A and classType=Yoga are returned And direct access to out-of-scope records via UI or API returns 404 NOT_FOUND_SCOPED And UI filters are pre-set to permitted scopes and locked And audit logs include applied scope filters (location, classType) for each query
Invoice Create/Edit Allowed; Refunds/Credits Prohibited
Given a role with invoice.create=true, invoice.edit=true, refunds.process=false, credits.issue=false When the user creates or edits an invoice Then the action succeeds subject to standard validations When the user attempts to process a refund or issue a credit from that invoice Then UI buttons are disabled with tooltip “Not permitted”; API calls return 403 FINANCIAL_ACTION_FORBIDDEN And an inline banner explains separation of duties and how to request access
Payment Capture Requires Elevated Permission or Owner Override
Given a role with payment.capture=false and an owner capable of override approval When the user attempts to capture a payment authorization Then an action gate prompts for owner confirmation and capture is blocked until approved And after owner approval in-session the capture proceeds and the audit trail attributes requester and approver And if no approval is granted, the capture remains blocked and is logged with type=CAPTURE_BLOCKED
PII Redaction and Field‑Level Permissions
"As a privacy‑conscious owner, I want sensitive client details masked by default so that staff only access PII when necessary."
Description

Protect client PII by introducing field‑level permissions and dynamic masking for sensitive fields (e.g., email, phone, payment details, notes). Render masked values unless explicitly allowed by scope; allow temporary reveal on authorized action with audit trail. Apply masking across UI, exports, API responses, notifications, and logs. Provide configurable PII definitions per region to support compliance. Expected outcome: privacy‑by‑default access that reduces leakage risk while preserving operational usefulness.

Acceptance Criteria
Default UI Masking for Unauthorized Users
Given a signed-in user without field-level permission to view email, phone, payment details, or notes for a client When they view the client profile, roster, attendance list, calendar, or search results Then those fields render masked per policy (email: first letter + domain only; phone: last 2 digits only; payment: last 4 digits only; notes: [REDACTED]) Given the same user When they inspect the page DOM, network responses, or attempt copy/paste/print Then the unmasked values are not present anywhere client-side and only masked values are retrievable Given the same user When they use quick views, tooltips, or inline edit forms Then sensitive fields remain masked and are not editable
Authorized Temporary Reveal With Audit Trail
Given a user with 'Reveal PII' permission for a specific field within their assigned scopes When they click Reveal and provide a justification Then the field is unmasked for a maximum of 120 seconds and automatically re-masked afterward Given the same action When Reveal is executed Then an immutable audit event is recorded with userId, clientId, field, scope context, justification, timestamp, and reveal duration Given missing permission, out-of-scope client, or no justification When the user attempts Reveal Then the request is denied and no data is exposed; an audit event is recorded for the denied attempt
Field-Level Permissions Enforced by Role and Scope
Given role, location, room, class type, client segment, and financial area scopes configured When a user accesses a client or payout outside their allowed scopes Then sensitive fields are masked and restricted actions (edit/export) are disabled Given the same configuration When the user accesses an in-scope client Then only fields explicitly permitted by their role are viewable/editable; all other sensitive fields remain masked Given a scope or role update When applied by an admin Then changes take effect on the user's next request and revoke previously granted access within 60 seconds for active sessions
API and Webhook Redaction
Given an API request authenticated with a token lacking pii.read scope or field permissions When calling client list/detail endpoints or receiving webhooks Then sensitive fields are masked or omitted and response metadata indicates redaction was applied Given a token with pii.read plus field-level permission and matching scopes When requesting specific clients Then only allowed sensitive fields return unmasked; all others remain masked Given any API error or validation message When it references user-supplied identifiers Then raw PII values are not echoed in messages, logs, or payloads
Export and Report Redaction and Blocking
Given a user without export permission for PII or with 'no exports' scope on financial areas When they attempt to export client or payout data Then the export is blocked with a 403 and an audit event records the attempt Given a user with export permission but without field-level permission for certain sensitive fields When they export reports (CSV, XLSX, PDF) Then disallowed fields are omitted or masked consistently with UI policy and the export indicates 'PII redacted' Given any scheduled or on-demand export When it completes Then the audit trail includes who exported, time, filters, fields included/masked, and file checksum
Downstream Channels Redaction (Notifications and Logs)
Given an unauthorized user previews or sends notifications (email/SMS/push) that reference client contact info When composing or previewing Then contact tokens render masked for the sender and the underlying addresses/numbers are not exposed in the UI Given notification delivery and storage When messages are queued, sent, or stored Then system logs, message archives, and analytics store masked values for PII fields; raw PII is never written to application logs or error traces Given an error occurs while sending a notification When the system logs the error Then logs contain redacted placeholders instead of raw PII and still provide sufficient diagnostics (IDs, timestamps, templates)
Regional PII Definitions and Enforcement
Given administrators configure PII field definitions per region (e.g., EU, US) When a client is tagged to a region Then masking and permission enforcement apply to the fields defined for that region across UI, exports, API, notifications, and logs Given a regional configuration change When saved Then the new definitions propagate and take effect system-wide within 5 minutes and are captured in an audit record with before/after diffs Given an undefined or conflicting region When accessing a client Then the system applies the global default PII configuration and flags the configuration issue for admin review
Comprehensive Access Audit Logging
"As an owner, I want a clear audit trail of who accessed or changed sensitive data so that I can meet compliance and resolve issues quickly."
Description

Record immutable audit events for permission checks (granted/denied), data access to sensitive entities, and all role or scope changes. Include who, what, when, where (IP/device), and why (triggering action). Provide an admin audit console with filters by user, scope, entity, and timeframe, plus export for compliance. Integrate alerts for repeated denials or high‑risk actions. Expected outcome: traceability for security reviews, faster incident response, and better configuration hygiene.

Acceptance Criteria
Permission Checks Logged (Granted & Denied)
Given a user performs an action that requires a ScopeGuard permission evaluation When the permission is evaluated as granted or denied Then an audit event is recorded exactly once including: event_id (UUID), outcome (granted|denied), actor_user_id, actor_role, actor_scopes, action_name, target_entity_type, target_entity_id (if applicable), timestamp_utc (ISO8601), ip_address, user_agent_or_device_fingerprint, request_id, trigger_source (UI|API|Integration), and policy_version And the event is append-only and cannot be modified or deleted by any role And the event becomes searchable in the admin audit console within 5 seconds of occurrence And duplicate events are prevented via idempotency on request_id
Sensitive Data Access Events Captured
Given a user views, exports, or modifies sensitive entities (e.g., client PII, payouts, bank details, invoices, attendance) When the data is accessed via UI or API Then an audit event is recorded with who, what, when, where (IP/device), and why (triggering action) And PII values are never stored in the event payload; only categories/fields touched (e.g., email, phone, DOB) are listed And export events include export_format, filter_params_hash, and record_count And read events are logged only for designated sensitive entities And the event is available in the audit console within 5 seconds
Role and Scope Changes Audited with Before/After and Justification
Given an admin or owner modifies a user’s role or ScopeGuard scopes (location, room, class type, client segment, financial areas) When the change is submitted Then the system requires a non-empty justification (minimum 10 characters) and blocks save otherwise And an audit event records actor_user_id, target_user_id, before_roles_scopes, after_roles_scopes, justification, effective_at_utc, timestamp_utc, ip_address, and request_id And revert actions create a new linked event referencing the original change event_id And all such events are immutable and appear in the audit console within 5 seconds
Admin Audit Console: Filtering, Scoping, and Display
Given an authorized user with Audit.View permission opens the Audit Console When they filter by user, scope, entity type, action (read|write|export|role_change|permission_check), outcome (granted|denied), and timeframe (absolute UTC or relative 15m/1h/24h/7d/30d) Then the results exactly match the filters, are sorted by timestamp desc by default, and show total count with server-side pagination And the viewer only sees events within their permitted scopes; users without Audit.View cannot access the console And each row displays: who, what, when (UTC and local), where (IP/device), why (triggering action), and correlation/request_id; sensitive values are redacted And performance returns first page within 2 seconds for up to 100k matching events
Compliance Export with Tamper-Evident Manifest
Given an authorized user with Audit.Export permission requests an export from the Audit Console When a filter and timeframe (up to 31 days per export) are applied Then matching events are exported to CSV and JSON with identical record sets to the on-screen results And the download includes a signed manifest containing org_id, filter_hash, time_range, row_count, generated_at_utc, and sha256 checksums for each file And the export completes within 2 minutes for up to 1,000,000 events or provides an async link to retrieve when ready And the export action itself is audited with who, when, where, and file identifiers And users without Audit.Export permission cannot initiate or view exports
Alerts for Repeated Denials and High-Risk Actions
Given alerting is enabled with default thresholds When a user accumulates 5 or more denied permission checks within 10 minutes, or performs a high-risk action (scope escalation to financial areas, PII export, payout view by non-finance role) Then an alert is generated within 60 seconds and delivered to configured channels (email and webhook/Slack) And alerts are deduplicated per user and rule for 15 minutes to prevent spam And alert configuration (thresholds, channels) is restricted to owners/security admins and changes are audited And alert acknowledgements and mutes (1h, 24h) are recorded as audit events
Delegated and Time‑Bound Access
"As an operations lead, I want to grant temporary, scoped access to fill gaps so that classes run smoothly without overexposing data."
Description

Enable owners and managers to delegate scoped access to substitutes or visiting instructors with time limits and automatic revocation. Support approval workflows for temporary elevation and immediate emergency revoke. Notify stakeholders before access expires and when elevations occur. Expected outcome: flexible staffing without compromising least‑privilege controls or leaving stale access in the system.

Acceptance Criteria
Create Time‑Bound Delegation with Granular Scope
Given I am an Owner or Manager with delegation permission When I create a delegation for a substitute/visiting instructor and select specific locations, rooms, class types, client segments, and financial area flags (e.g., view‑only payouts, no exports) And I set a start datetime (>= now) and an end datetime (> start) Then the system validates the request is a subset of my own permissions and within org policy limits (e.g., max duration 90 days) And Save is enabled only after all validations pass When I save Then a delegation record is created with the exact scope and time bounds And before the start time the delegate cannot access any scoped resources And at/after the start time the delegate can only see and act within the selected scope in both UI and API
Automatic Expiry and Session Revocation
Given a delegation is active When the current time reaches the delegation end datetime Then the delegate’s elevated access is revoked within 60 seconds And active sessions lose elevated capabilities immediately (subsequent protected actions return 403) And API tokens tied to the delegation are invalidated And an audit log entry records auto‑revocation by expiry
Emergency Immediate Revoke
Given a delegation is active When an Owner or Manager triggers Emergency Revoke Then elevated access is revoked within 30 seconds across UI and API And the delegate is signed out of protected areas or prevented from performing protected actions immediately (403 on retry) And stakeholders and the delegate receive an immediate revoke notification And the delegation cannot be re‑enabled; a new approval flow is required
Approval Workflow for Temporary Elevation
Given a requested delegation includes elevated permissions beyond the user’s baseline role When the requester submits the scope, start, and end times Then an approval task is created and routed to the designated approver(s) and they are notified And access is not granted until approved When an approver approves Then the delegation is created with the approved scope and time bounds and the requester and delegate are notified When an approver rejects or the request times out after 48 hours Then no access is granted and the requester is notified And all actions (request, approve, reject, timeout) are audit‑logged
Notifications for Elevations, Reminders, and Expiry
Given a delegation is created or approved Then notify the delegate and relevant owners/managers with scope and duration details via in‑app and email Given a delegation will expire When 24 hours remain (or 1 hour if created <24 hours prior) Then send an expiry reminder to the same stakeholders When a delegation expires or is revoked Then send an immediate termination notification And duplicate notifications are suppressed within a 10‑minute window And mandatory security notifications cannot be opted out
Overlapping Delegations and Least‑Privilege Enforcement
Given a user has a baseline role and multiple time‑bound delegations Then effective permissions equal baseline plus the union of delegations, constrained to each delegation’s scope and time window And denies on sensitive permissions (e.g., exports) override allows when scopes conflict When one delegation expires or is revoked Then effective permissions are recomputed within 60 seconds And no delegation may grant permissions the delegator does not possess And behavior is consistent across web, mobile, and API
Comprehensive Audit Logging and Reporting
Given any delegation lifecycle event (create, update, approve, reject, revoke, expire) Then an immutable audit record is created capturing actor, target user, action, timestamp (UTC), IP/device, scope details, and time bounds And audit records are readable by Owners/Managers with audit permission and retained for 7 years And audit logs are searchable by user, date range, location, and action And export of audit logs is available to Owners only and recorded as an audit event And system clocks are synchronized to keep audit timestamps within 2 seconds across services

Sunset Access

Time‑boxed access with auto‑expire and renewal rules for subs, contractors, and auditors. Set start/end dates, tie expiry to the last assigned class, and send proactive reminders before access lapses—eliminating forgotten accounts and tightening security without manual follow‑ups.

Requirements

Role-Based Access Window Configuration
"As a studio owner, I want to define start and end dates for each contractor’s access, with role-based defaults, so that access is time-boxed without manual tracking"
Description

Provide administrators with controls to set and manage access start and end dates per user and per role (e.g., substitute, contractor, auditor), with organization- and role-level defaults. Support absolute dates and relative rules such as “expire N days after last assigned class ends,” time zone selection per user or org, and optional grace periods. Include batch edit, CSV import, and API endpoints to create/update access windows. Validate for overlaps, daylight saving transitions, and conflicting policies. Persist policy metadata for auditability and apply uniformly across web and mobile surfaces

Acceptance Criteria
Organization-Level Defaults and Role-Based Overrides
Given I am an organization admin with permission to manage access policies When I set default access windows for roles (substitute, contractor, auditor) using either absolute dates or a relative rule (expire N days after last assigned class ends) and select the organization time zone Then the defaults are saved and immediately applied to newly created users in those roles And inherited values are visible on user profiles and explicitly labeled as "Inherited" And end time must not precede start time; invalid inputs block save with field-level errors And an audit record is captured with actor, timestamp, scope (org/role), and before/after values And the same defaults and labels render identically in web and mobile admin interfaces
Per-User Role Access Window Configuration and Validation
Given a user with one or more roles at the organization When I override the role default and set a per-user access start and end using absolute dates or a relative rule and optionally add a grace period in days and choose a time zone (user or organization) Then the system displays the computed effective window in the selected time zone before save And saving persists the override, marks the policy as "Override," and returns success And overlapping windows for the same user-role are prevented with a descriptive error And conflicting field entries (e.g., both absolute end and relative rule simultaneously) are blocked with guidance And an audit record is stored with user, role, actor, calculation basis, and timestamps
Relative Expiry Tied to Last Assigned Class with Grace Period
Given a user has a role whose access end is defined as "expire N days after last assigned class ends" with optional grace period G days and a selected time zone When the user's last assigned class end time changes (new assignment added, rescheduled, or removed) Then the effective expiry recalculates to last_assigned_class_end + N days + G days in the selected time zone And if N and G are zero, access ends at the exact last assigned class end time And if a future class is assigned that ends later than the current last assigned class, the expiry updates accordingly And recalculation events are logged in the audit trail with previous and new expiry timestamps
Time Zone Selection and Daylight Saving Transition Handling
Given a policy with start/end in a specified IANA time zone (user or organization) When a start or end falls on a daylight saving transition (spring forward or fall back) Then the boundary is applied using wall-clock local time with no unintended one-hour gain or loss of access And the computed UTC timestamps are correct and consistent across API, web, and mobile clients And switching the selected time zone immediately recalculates and displays new effective start/end before save And validation prevents ambiguous or nonexistent local times by prompting for the intended offset
Batch Edit and CSV Import of Access Windows
Given I upload a CSV containing user identifier, role, start, end or relative rule parameters, time zone, and optional grace period When I run the import Then valid rows are applied; invalid rows are rejected with row-level error codes and messages downloadable as a report And partial success is allowed without aborting the entire file And existing access windows for listed user-roles are updated; non-listed user-roles remain unchanged And overlapping or conflicting policies detected during import are rejected per-row with reasons And all successful changes write audit records referencing the import job id And batch edit via UI produces identical results to a CSV import with the same inputs
API Create/Update Access Windows with Validation and Idempotency
Given an authenticated client with scope to manage access windows When it calls the API to create or update a user-role access window with absolute or relative parameters, time zone, and optional grace period Then requests with valid payloads succeed (201 on create, 200 on update) and return the stored resource including computed effective start/end and policy metadata And invalid requests return 4xx with machine-readable error codes per invalid field And an Idempotency-Key header on create prevents duplicate windows on retries, returning the original result And concurrent updates are protected (e.g., via ETag/If-Match or version field); stale updates are rejected with a 409 And every API write is captured in the audit log with actor (API client), request id, and before/after values
Conflict Detection, Precedence Resolution, and Uniform Enforcement
Given organization defaults, role defaults, and per-user overrides may specify different rules When multiple policies apply to the same user-role Then precedence is applied as: per-user override > role default > organization default, and the effective policy is displayed in UI and returned by API And any conflicting or mutually exclusive settings within the same scope are blocked with clear errors And after the effective end is reached, the user loses access to protected actions on both web and mobile surfaces on the next request And updating the policy to extend access restores permissions immediately after save And enforcement and displayed status are consistent across web, mobile, and API responses
Auto-Expire and Deprovision Engine
"As an operations manager, I want access to auto-expire and deprovision at the configured time so that lapsed users can no longer manage classes or see rosters, improving security without manual work"
Description

Implement a reliable scheduler and rules engine that automatically revokes permissions at the moment of expiry in the configured time zone. On expiry, disable sign-in to admin areas, block class assignment and roster access per role, and prevent booking or payout-sensitive actions as appropriate. Ensure idempotent, retry-safe operations with observability (metrics, alerts) and partial-failure recovery. Do not delete historical data or past attendance; preserve records while removing forward-looking capabilities. Provide configurable grace periods and a reversible “Reinstate” action that restores prior scopes. Enforce across APIs and UI to prevent bypass

Acceptance Criteria
Expiry Executes at Configured Time Zone Boundary
Given a user with Sunset Access configured to expire at 2025-09-07T23:59:00 in time zone America/New_York When the local time in America/New_York reaches 2025-09-07T23:59:00 Then the system deprovisions the user within 60 seconds of that moment and not earlier And the deprovision respects DST/zone rules for the configured time zone And an audit event "access.expired" with the user ID, org ID, and effective timestamp is recorded
Admin Sign‑In Block and Session Invalidation on Expiry
Given a user whose Sunset Access has expired When the user attempts to sign in to any admin area via Web UI or API Then the request is rejected with HTTP 401/403 and error code ACCESS_EXPIRED And any existing admin sessions and refresh tokens are invalidated within 60 seconds And subsequent OAuth/SSO token exchanges are denied with ACCESS_EXPIRED
Role De‑Scoping: Block Class Assignment, Roster Access, Booking, and Payout Actions
Given a contractor/instructor/auditor whose Sunset Access has expired When they attempt any of: (a) being assigned to a class, (b) viewing or exporting class rosters, (c) creating/modifying bookings, (d) initiating refunds or viewing payouts Then the UI disables the action and the API returns HTTP 403 with error code ACCESS_EXPIRED And no downstream emails, notifications, or financial side effects are triggered And the block is enforced consistently across UI and all public/private APIs
Historical Data Preserved; Forward‑Looking Capabilities Removed
Given a user reaches Sunset Access expiry Then no historical records (past attendance, invoices, payouts, audit logs) are deleted or altered And organization owners/admins retain read access to these historical records and reports are unchanged And the expired user is excluded from being suggested for future class assignments or payout runs
Idempotent and Retry‑Safe Deprovision Operations
Given the deprovision job is triggered multiple times (retries or duplicate scheduling) When the job executes concurrently on the same user Then the final authorization state is consistent (deprovisioned once) And no duplicate side effects occur (no duplicate audit entries, notifications, or token revocations) And the job can be safely retried until success without manual intervention
Observability with Alerts and Partial‑Failure Handling
Given the Auto‑Expire pipeline processes events Then metrics are emitted: expiries_scheduled, expiries_succeeded, expiries_failed, time_to_deprovision_p50/p95, queue_age, retries_count And an alert is raised within 5 minutes if expiries_failed > 1% over 15 minutes or queue_age > 5 minutes And on partial failures (e.g., token revocation fails), the system retries with exponential backoff up to a configurable limit and achieves eventual consistency within 10 minutes or pages on‑call
Grace Period and Reinstate Restore Prior Scopes
Given a configurable grace period (e.g., 48 hours) is set for Sunset Access When the nominal expiry time passes Then restrictions are applied only after the grace period ends, and attempts during grace show warning state GRACE_ACTIVE And when an authorized admin clicks Reinstate during or after expiry Then the user’s prior scopes/roles and admin sign‑in are restored exactly as before expiry within 60 seconds And Reinstate is idempotent and recorded with audit event "access.reinstated" including actor and reason
Proactive Renewal Reminders and Escalations
"As an administrator, I want automated reminders before a contractor’s access expires so that I can renew on time and avoid last-minute disruptions"
Description

Enable configurable reminder cadences (e.g., 14/7/3/1 days and day-of) that notify both the expiring user and designated admins via email, SMS, and in-app notifications. Include localized, templated messages with dynamic fields (name, role, expiry date, classes affected) and deep links for one-click admin renewal with tokenized security. Support snooze/dismiss, escalation rules when no action is taken, and automatic calendar holds if policy requires. Log delivery status and user actions for audit and reporting

Acceptance Criteria
Configurable Reminder Cadence and Timing
Given a tenant configures a reminder cadence of [14, 7, 3, 1, 0] days before access expiry and the tenant timezone is set When the scheduler runs at the configured send window Then reminders for each recipient are scheduled for the exact times corresponding to 14d, 7d, 3d, 1d, and day-of expiry in the tenant timezone Given a user’s expiry date or timezone is updated When the scheduler recalculates upcoming reminders Then only future reminders are re-scheduled to align with the new settings and no duplicate reminders are created Given a reminder was already sent for a cadence point When the scheduler re-runs due to retries or failures Then the same reminder is not sent twice (idempotent send) Given a user’s access is renewed or revoked before the next scheduled reminder When the scheduler evaluates pending reminders Then all future reminders for that expiry cycle are canceled Given daylight saving transitions occur in the tenant timezone When reminders are due around the transition Then reminders are delivered at the correct local wall-clock time as configured
Localized Multi-Channel Notifications to Users and Admins
Given an expiring user and designated admins with channel preferences and contact info When a reminder is triggered Then the expiring user and all designated admins each receive notifications via enabled channels (email, SMS, in-app), with channel-specific templates applied Given message templates exist per locale with dynamic fields {recipient_name}, {recipient_role}, {expiry_date}, {classes_affected} When a notification is generated Then all dynamic fields are populated accurately for the recipient and locale, and the message renders without placeholder tokens Given a user has a preferred locale and the tenant has a default locale When a notification is generated Then the system selects the user’s locale; if unavailable, it falls back to the tenant default, and if still unavailable, to en-US Given a recipient lacks contact data for a channel (e.g., no mobile number) When the notification is sent Then the system skips that channel and successfully delivers on the remaining enabled channels without error Given an admin is designated via multiple groups or roles When a reminder is sent Then the admin receives only one notification per cadence point (deduplicated) Given an in-app notification is delivered When the recipient views it Then it displays actionable buttons/links for Renew (admins), Snooze, and Dismiss
Tokenized One-Click Admin Renewal Deep Link
Given an admin recipient receives a reminder with a deep link When the admin clicks the link within the token’s validity window Then the system validates a signed, single-use token scoped to the tenant, expiring user, expiry cycle, and admin identity and proceeds to renew access per policy without requiring additional navigation Given the token is expired, invalid, revoked, or already used When the deep link is opened Then renewal is not performed and the admin is shown an error state; the event is logged with reason Given the admin lacks renewal permission When the deep link is used Then the system rejects the action with a 403-equivalent outcome and logs the attempt Given the admin’s device has the ClassTap app installed When the deep link is opened Then the link deep-opens into the app; otherwise, it opens a secure web flow with the same one-click action Given a renewal completes via deep link When the operation succeeds Then the expiring user’s access end date is extended per tenant policy, all future reminders for that cycle are canceled, and an audit log entry is created
Snooze and Dismiss Behavior
Given a recipient receives a reminder When the recipient selects Snooze with a configured snooze interval (e.g., 3 days) Then no reminders are sent to that recipient for that expiry cycle during the snooze interval, and the next reminder is re-scheduled accordingly without exceeding the expiry date Given a recipient receives a reminder When the recipient selects Dismiss Then no further reminders for that expiry cycle are sent to that recipient across all channels, while other recipients continue to receive per policy Given a recipient snoozes multiple times within an expiry cycle When the scheduler evaluates upcoming reminders Then cumulative snoozes never schedule reminders past day-of expiry; if the snooze would push past expiry, the next reminder is set to day-of at the configured send time Given a snooze or dismiss action is taken from email, SMS, or in-app When the system processes the action Then the action is authenticated, recorded, reflected immediately in scheduling, and confirmed to the user
No-Action Escalation Rules
Given an escalation rule is configured for no-action after the final reminder When the final reminder passes and no renewal or dismissal by an authorized admin has occurred within the configured window Then an escalation notification is sent to the escalation recipient list via configured channels with a summary of the user and impacted classes Given an escalation has been sent When the expiring user is renewed Then further escalations for that expiry cycle stop and the escalation thread is marked resolved in logs Given multiple users reach escalation simultaneously When escalations are sent Then recipients receive a single consolidated escalation per batch window (if configured), not one per user, and the content lists all affected users Given escalation delivery fails on one channel When alternative channels are available Then the system attempts the remaining channels once and records the outcome per channel
Automatic Calendar Holds per Policy
Given the tenant enables automatic calendar holds for impending expiries with a lead time (e.g., 3 days) When a user is within the lead time window prior to expiry Then classes assigned to that user occurring after the expiry are placed on Hold and new class assignments after the expiry are blocked Given holds are created due to impending expiry When the user’s access is renewed Then all related holds are automatically released and class assignments are restored Given holds are created due to impending expiry When the access lapses without renewal Then holds convert according to policy (e.g., Unassigned or Canceled) and no attendance or payments are processed under the expired access Given calendar holds are applied When viewing the schedule Then a visible banner or icon indicates the hold reason and expiry date to instructors and admins
Audit Logging for Delivery and Actions
Given a reminder is attempted on any channel When the send is processed Then an immutable log entry is created with timestamp, recipient, channel, template/version, locale, correlation/message ID, and delivery status (queued, sent, delivered, failed, bounced) Given a recipient interacts with a notification (deep link click, Snooze, Dismiss, Renew) When the action completes Then an audit log entry records actor, action type, target user, channel, IP/device fingerprint (where available), outcome, and timestamp Given auditors or admins access reporting When they query by date range, recipient, channel, status, or action Then matching delivery and action logs are retrievable within 3 seconds for 95th percentile and exportable to CSV Given an admin views a user’s Sunset Access history When the audit trail is displayed Then all reminders, escalations, holds, renewals, and outcomes for the current and prior expiry cycles are shown in chronological order
Assignment-Aware Expiry Rules
"As a scheduler, I want the system to block or auto-extend access when assigning classes past a user’s expiry so that assignments never violate access policies"
Description

Tie access validity to class assignments and scheduling. Prevent assigning an expiring or expired user to classes beyond their access window unless policy allows auto-extension. If allowed, auto-extend using configured rules (e.g., extend through the last assigned class plus N days) and prompt for confirmation and cost center if applicable. Handle waitlist-to-assignment flows by checking access at assignment time and at auto-promotion time. Surface conflicts inline, propose remediation options, and ensure transactional integrity so bookings and access stay consistent

Acceptance Criteria
Block Assignment When Access Ends Before Class Date
Given a user whose access end datetime is before the selected class start datetime And the organization policy is set to "No auto-extension" When a scheduler attempts to assign the user to that class Then the assignment is blocked And an inline error is shown stating the access end datetime and the policy reason And the Assign action is disabled And no booking, waitlist change, or access record is created or modified And an access policy violation event is written to the audit log
Auto-Extend On Assignment With Confirmation And Cost Center
Given the organization policy allows auto-extension using the rule "extend through last assigned class end + N days" And the user’s current access end datetime is before the selected class end datetime When the scheduler clicks Assign Then the system proposes a new access end datetime computed from the class end + N days And prompts the scheduler to confirm the extension And requires cost center selection if the organization setting "Cost center required on extension" is enabled And upon confirmation, the access extension and booking are committed atomically And the assignment appears on the schedule, and the user’s access end datetime reflects the new value And an audit log entry records the old and new access dates, rule used, approver, and cost center And if the scheduler cancels, no changes are persisted and the user is not assigned
Waitlist Auto-Promotion Access Check And Remediation
Given a user is on the waitlist for a class And their access end datetime is before the class start datetime When the auto-promotion job runs Then the system evaluates access against policy And if policy forbids auto-extension, the promotion is skipped and a notification is sent to the scheduler explaining the reason And if policy allows auto-extension and the organization setting "Auto-extend on auto-promotion without prompt" is enabled, the system auto-extends per rule and promotes the user atomically, logging the change And if policy allows auto-extension but confirmation or cost center is required, the system does not promote and creates an actionable alert requesting approval; the user remains on the waitlist until actioned
Inline Conflict Surfacing And Guided Remediation
Given a scheduler selects a user and class where the class occurs outside the user’s current access window When the assignment form opens Then an inline conflict banner appears showing the user’s access start/end datetimes and the conflicting class datetime And the banner offers remediation actions: "Extend access", "Pick another date within window", and "Cancel" And choosing "Extend access" displays the computed new end datetime per rule and any cost center requirement prior to confirmation And choosing "Pick another date within window" filters the date picker to in-window dates And after any remediation is confirmed, the conflict banner disappears and the form reflects the resolved state
Transactional Integrity For Access Updates And Bookings
Given an assignment requires access extension per policy When the scheduler confirms the extension and assignment Then the system performs the access update and booking in a single transaction And if the access update fails, the booking is not created And if the booking fails, the access update is not applied And retry operations are idempotent, preventing duplicate extensions or duplicate bookings And the final state leaves access and booking consistent with the outcome
Bulk And Drag-and-Drop Assignment Enforcement
Given a scheduler performs bulk assignment or drag-and-drop scheduling for multiple users/classes When any target assignment falls outside a user’s access window Then the system evaluates each assignment against policy And blocks non-extendable assignments with per-item inline errors And for extendable items, prompts once to confirm extensions summarizing impacted users, new end datetimes, and any required cost centers And only the confirmed extendable assignments are committed atomically with their respective access updates; all others are left unchanged And a summary report lists successes and failures by item with reasons
Access State Indicators and Blocking UI
"As a studio admin, I want clear visual indicators and actionable prompts when a user is near or past expiry so that I can quickly take the right action or avoid blocked workflows"
Description

Display clear access state across profiles, rosters, class modals, and assignment lists, including badges such as Active, Expiring Soon, Grace, and Expired with countdown timers. Provide contextual CTAs like Renew, Extend, or Reinstate depending on permissions. Gracefully block restricted actions with inline explanations and links to policy settings. Ensure consistent behavior on mobile and desktop, with accessibility compliance for colors and screen readers

Acceptance Criteria
Consistent State Badge Display Across Surfaces
Given a user views any of the following surfaces: Instructor Profile, Class Roster, Class Details Modal, Assignment List When the subject has an access state of Active, Expiring Soon, Grace, or Expired Then a visible badge matching that state is displayed on the surface And the badge text, color, and icon variant are identical across all surfaces for the same state And the badge includes a tooltip or aria-label describing the state and time remaining when applicable And when no Sunset Access applies, no access state badge is rendered
Countdown Timers and Timezone Accuracy
Given a subject is in Expiring Soon or Grace state When the user views the countdown timer Then the remaining time is shown in days and hours (e.g., 2d 04h) and updates at least every 60 seconds And the countdown is calculated using the organization timezone setting And when the remaining time reaches 0, the state updates to Expired and the UI reflects the new state within 60 seconds without page reload And the accessible label conveys the absolute expiration timestamp including timezone
Contextual CTAs by State and Permission
Given a viewer has Manage Access permission When viewing a subject with Active state Then an Extend CTA is visible and enabled And Renew or Reinstate CTAs are not shown Given a viewer has Manage Access permission When viewing a subject with Expiring Soon or Grace state Then Extend and Renew CTAs are visible and enabled Given a viewer has Manage Access permission When viewing a subject with Expired state Then Reinstate and Renew CTAs are visible and enabled Given a viewer lacks Manage Access permission When viewing any subject state Then state badges are visible And all access-management CTAs are not rendered And clicking any visible CTA opens the correct flow modal specific to the action
Action Blocking With Inline Explanation and Policy Link
Given a restricted action is attempted (e.g., assign to class, check-in) for a subject And policy marks the action as disallowed for the subject's current state When the user initiates the action Then the action is blocked without navigation away from the current context And an inline message appears stating the state, the reason for the block, and time remaining if applicable And the message includes a link labeled "View access policy" that navigates to policy settings in a new tab And if the user has Manage Access permission, contextual CTAs (Extend, Reinstate, or Renew) are displayed in the message; otherwise, they are not shown And the block message is announced to screen readers via an aria-live polite region
Accessibility Compliance for State Indicators
Given state badges and countdown timers are rendered Then text and icon contrast meet WCAG 2.1 AA (text contrast ≥ 4.5:1; non-text ≥ 3:1) And state is not conveyed by color alone: each badge includes an icon and text label And badges and countdowns have accessible names that include state and absolute expiry And interactive elements (CTAs, tooltips) are keyboard accessible: Tab focusable, Enter/Space activates, Esc closes overlays And when a blocking message appears, focus moves to the message container and returns to the triggering control on dismiss
Mobile and Desktop Consistency and Performance
Given the app is viewed on mobile (≤768px) and desktop (>768px) When state badges, countdowns, and CTAs render on profiles, rosters, class modals, and assignment lists Then layout does not overflow, overlap, or truncate essential text on either form factor And touch targets for CTAs on mobile are at least 44px in height and width And countdowns use abbreviated format on mobile (e.g., 2d 4h) without losing meaning And initial render of badges and countdowns occurs within 500ms on broadband and 1500ms on 3G And orientation changes reflow components without visual artifacts And state changes are reflected via polling or subscription at least every 60 seconds without full page reload
Dynamic Badge Update on Last Assigned Class Change
Given expiry is configured to tie to the last assigned class And the user has a roster or profile view open When a class assignment is added or removed that changes the effective expiry Then the access state badge and countdown update on all open surfaces within 60 seconds without page reload And the updated state and time remaining are consistent across profile, roster, class modal, and assignment list
Access Audit Trail, Reports, and Webhooks
"As a compliance officer, I want a complete audit trail and event hooks for access changes so that I can demonstrate control and integrate with external systems"
Description

Record an immutable audit log of access window creations, edits, renewals, expiries, auto-extensions, and notifications, including actor, timestamp, rationale, and policy snapshot. Provide filters, exports (CSV), and summary dashboards (active vs. expiring, renewal rates, exceptions). Emit webhooks for key events (about-to-expire, expired, renewed, deprovisioned) for downstream integrations. Ensure logs are retained per data retention policy and are searchable via API

Acceptance Criteria
Immutable Audit Logging for Sunset Access Events
Given any of the following events occurs: access_window_created, access_window_edited, access_window_renewed, access_window_expired, access_window_auto_extended, access_notification_sent, access_deprovisioned When the event is persisted Then an audit record is appended with fields: event_type, tenant_id, actor_type, actor_id, actor_display_name, target_type, target_id, occurred_at (UTC ISO 8601), rationale (optional, ≤500 chars), policy_snapshot (JSON), request_id, ip, user_agent, version, checksum And the record is immutable: update and delete operations are not available; any attempt returns 405 and is itself logged And all timestamps are server-generated; client-supplied timestamps are ignored And policy_snapshot reflects the effective Sunset Access policy at the time of the event
Log Filtering, Sorting, and Performance in UI
Given a tenant with ≥1,000,000 audit records When a user applies filters (date range up to 90 days, event types, actor, target, outcome, policy name) and sorts by occurred_at desc Then the first page (50 rows) renders within ≤2 seconds at p95 And applied filters are accurately reflected in results and encoded in a shareable URL And pagination returns consistent, non-duplicated records across pages And timestamps display in the org’s local timezone with hover to reveal UTC
CSV Export of Audit Logs with Filter Fidelity
Given current UI filters are applied to the audit log When the user requests a CSV export Then the file includes only matching records with a header row and columns: occurred_at_utc, event_type, tenant_id, actor_type, actor_id, target_type, target_id, rationale, policy_snapshot, request_id, ip, user_agent And values are UTF-8, comma-separated, RFC4180-compliant, quoting fields containing commas And for ≤200,000 rows the export completes within ≤5 minutes and is delivered via a signed URL that expires in 7 days And for >200,000 rows the user is prompted to narrow filters before exporting And all timestamps in the CSV are normalized to UTC ISO 8601
Summary Dashboards: Active vs. Expiring, Renewal Rates, Exceptions
Given audit logs exist for the tenant When the dashboard is loaded for a selected window (last 30 or 90 days) Then widgets display: (1) active access windows, (2) expiring in next 7 and 30 days, (3) renewal rate for the window, (4) exceptions: manual overrides, auto-extension failures, failed notifications, deprovision delays >24h And each widget links to a pre-filtered audit log view that matches the displayed counts And dashboard counts reconcile with underlying logs for the same filters (variance ≤1 record)
Webhooks for Access Lifecycle Events with Secure Delivery
Given webhook destinations are configured and enabled for the tenant When any of the following events occurs: about_to_expire, expired, renewed, deprovisioned Then a POST is sent within ≤60 seconds with payload: event_id (UUID), event_type, tenant_id, occurred_at (UTC), idempotency_key, resource: {access_window_id, user_id, class_id (if applicable), policy_snapshot} And the request is signed with HMAC-SHA256 using the tenant secret and includes CT-Signature and CT-Timestamp headers And endpoints must be HTTPS (TLS 1.2+); insecure URLs are rejected at configuration time And a 2xx response marks success; non-2xx triggers up to 5 retries with exponential backoff over 30 minutes, preserving ordering per resource And duplicates are prevented by honoring idempotency_key; after final failure the event moves to a dead-letter queue and an admin alert is issued
Log Retention Enforcement per Data Policy
Given the organization’s data retention policy is configured between 3 and 36 months (default 24) When an audit record exceeds the retention window Then it is permanently deleted by a daily job and a retention-deletion summary (counts by day) is written to a compliance log And API/UI queries for deleted records return 410 Gone with error code AUDIT_LOG_EXPIRED And the UI displays the active retention policy and next purge date And exports and dashboards exclude deleted data
Searchable Audit Logs via Public API
Given a client has an OAuth2 token with scope audit.read When it calls GET /api/v1/audit-logs with filters (event_type[], actor_id, target_id, occurred_at_from, occurred_at_to, outcome, policy_name) and pagination (limit ≤1000, cursor) Then the API returns 200 with records matching filters, sorted by occurred_at desc, plus next_cursor when more pages are available And a snapshot cursor guarantees consistent pagination for 15 minutes And p95 response time is ≤500 ms for result sets ≤1000 records on a dataset of 10M records And invalid filters return 400 with structured error codes; unauthorized or insufficient scope returns 401/403 And per-token rate limiting of 60 requests/min is enforced with Retry-After headers

Two‑Key Approvals

Dual‑approval workflow for sensitive actions like granting financial permissions, exporting client lists, or editing policies. Capture a business reason, approve via email/Slack, and log every decision—preventing unilateral changes and strengthening accountability.

Requirements

Policy & Scope Configuration
"As an owner, I want to define which actions require two approvals and who can approve so that sensitive changes cannot be made unilaterally."
Description

Provide an admin interface and backend policy engine to define which actions require two-key approval, including financial permission grants, exporting client lists, and editing organizational policies. Support scoping rules by role, location, amount thresholds, and action category; map approver pools by role or named users; and allow per-organization settings with defaults. Include a safe “test mode” to simulate policies, versioning of policy changes, and migration tooling to bootstrap from current settings. Integrate with ClassTap’s RBAC and action middleware so policies are enforced consistently across web, mobile, and API. Expected outcomes: flexible governance without blocking normal operations and reduced risk of unilateral sensitive changes.

Acceptance Criteria
Scope rules by role, location, amount threshold, and category
Given Organization Alpha has a policy rule with action = grant_financial_permission, roles = [Manager], locations = [SF, NY], amount_threshold = >= 5000, category = Financial When a user with role Manager in SF initiates a grant for $5,000 Then the policy engine decision is requires_two_key and the matched_rule_id is returned And when the same user initiates a grant for $4,999 the decision is does_not_require_two_key And when a user with role Instructor in SF initiates a grant for $10,000 the decision is does_not_require_two_key And when a user with role Manager in LA initiates a grant for $10,000 the decision is does_not_require_two_key
Approver pools mapped by role and named users
Given a policy for category Financial defines approver_pool roles = [Owner, Finance], named_users = [user_123], min_approvals = 2 And the initiator is user_456 (role Manager, location SF) When an action in category Financial is initiated in location SF Then the computed approver_candidates = (all SF users with roles Owner or Finance) ∪ {user_123} minus {user_456} And approver_candidates contains unique users and excludes the initiator And the policy engine returns min_approvals = 2 and approver_candidates_count >= 2
Per-organization defaults and overrides
Given a global default policy template T v1 where export_client_list requires_two_key for all roles And Organization Beta has no overrides When Organization Beta policies are queried Then the effective policy includes export_client_list requires_two_key from template T v1 When Organization Beta creates an override so export_client_list requires_two_key only for roles [Manager] Then the effective policy for Organization Beta applies two-key only to role Manager and not to other roles And when template T updates to v2 for unrelated rules the effective policy for Organization Beta reflects v2 for non-overridden rules and retains the override for export_client_list
Test mode simulation without blocking actions
Given Organization Gamma has policy test_mode = enabled When a user initiates an action that would match a two-key rule via web, mobile, or API Then the decision is simulate_requires_two_key and the action proceeds without creating approval tasks or notifications And a simulation event is logged with correlation_id, matched_rule_id, computed_approver_pool, and channel (web|mobile|api) And API responses include header X-Policy-Simulation: true for simulated decisions
Policy change versioning and audit trail
Given Organization Delta has current policy version v3 When admin user a001 edits a rule and publishes with change_note = "raise export threshold" Then a new immutable version v4 is created with timestamp, editor_id = a001, change_note, and a machine-readable diff And the audit log records the change with previous_version = v3 and new_version = v4 When a rollback to v3 is performed Then version v5 is created restoring the v3 rule set and the audit trail shows rollback_from = v4, rollback_to = v3
Migration bootstrap from existing RBAC and settings
Given existing RBAC roles and legacy sensitive-action settings are present When the migration tool runs in dry-run mode Then it produces a proposed policy plan with mapping_coverage_percent, a list of unmapped items, and makes no writes to the policy store When the migration is approved and executed Then per-organization policy version v1 is created from the plan with migration_source = rbac_bootstrap and a migration report is archived And any unmapped items are flagged for admin follow-up without blocking normal operations
Consistent enforcement across web, mobile, and API via middleware
Given Organization Echo has policies enabled and test_mode = false When the same sensitive action request (same actor, role, location, amount, category) is initiated via Web UI, iOS app, and REST API Then the policy engine returns identical decisions and approver pools across all channels and records one decision per unique action_id And actions not covered by any policy rule do not trigger two-key checks in any channel And if a rule for the action requires a non-empty business_reason field, requests missing this field are rejected with error_code = missing_business_reason in all channels
Request Submission & Reason Capture
"As a staff user initiating a sensitive action, I want to submit a request with a clear business reason so that approvers have the context to decide quickly and accurately."
Description

Insert a standardized pre-action gate that transforms sensitive actions into approval requests. Require a structured business reason, category, and relevant fields (e.g., amount, client segment), with optional attachments. Capture a cryptographic hash of the intended change payload and a snapshot of current state to ensure the final approved action matches the request. Validate inputs, prevent bypass via direct API calls, and present a clear confirmation UI to the requester. Store drafts and support cancellation with traceability. Expected outcomes: richer context for decisions and faster, better-quality approvals.

Acceptance Criteria
Intercept Sensitive Action and Convert to Approval Request
Given a user initiates a sensitive action (grant financial permissions, export client lists, or edit policies) from the UI or API When the action is triggered Then the system opens a standardized approval request form and creates a Pending request record And the original action is not executed and no side effects occur until the request reaches Approved status And the request record includes requester ID, action type, affected resources, timestamp, and a unique Request ID And if the user lacks permission to request for this action, the system returns 403 and no request is created And an audit log entry is recorded for the initiation event
Structured Reason, Category, and Field Validation
Given the standardized request form is displayed When the requester attempts to submit Then Reason is required (20–500 characters, no all-whitespace) and must pass profanity/XSS validation And Category is required and must be one of: Finance, Data Export, Policy, Other And when Category = Finance, Amount is required, numeric > 0, max 999,999.99, 2 decimal places And when Category = Data Export, at least one Client Segment from the system list must be selected (max 5) And all invalid fields show inline error messages and the form cannot be submitted until all validations pass And all submitted values are persisted exactly as entered (normalized formats applied for currency and enums)
Optional Attachments with Security and Size Constraints
Given the requester uploads attachments When files are added Then only PDF, PNG, JPG/JPEG, and CSV are accepted; all others are rejected with an error And each file must be ≤ 10 MB and total size ≤ 25 MB; otherwise, upload is blocked with a clear message And each file passes a malware scan before it is associated to the request; failing files are rejected and not stored And successfully stored files are encrypted at rest and listed with filename, size, and removal option prior to submission
Hash and State Snapshot Integrity Enforcement
Given a requester submits a valid approval request When the request is submitted Then the system computes a SHA-256 hash of the canonical intended change payload and stores it immutably with the request And the system captures a read-only snapshot of the current relevant state (record IDs and version numbers) at submission time And at approval/execution time, the system recomputes the payload hash and compares it to the stored hash And if the hash differs or any current state version != snapshot version, execution is blocked, the request moves to Mismatch state, and the requester is notified to resubmit And the stored hash and snapshot metadata are visible in the request’s audit details and cannot be edited
Confirmation UI and Requester Acknowledgment
Given the requester has passed all field validations When they click Review & Submit Then a confirmation view summarizes the action, reason, category, dependent fields, attachments, impacted entities count, and shows the first 8 chars of the payload hash And the requester must explicitly confirm to submit; cancellation returns to edit mode without creating duplicates And upon submission, a success banner displays the Request ID and an email confirmation is sent to the requester within 60 seconds And the confirmation view loads within 2 seconds at the 95th percentile
Draft Save, Resume, and Cancellation with Audit Trail
Given the requester is filling the approval form When they click Save Draft or after 30 seconds of inactivity with unsaved changes Then the system saves a draft with all entered data and shows a Last saved timestamp And drafts are visible under My Drafts and can be resumed to the same step with all data intact And when a Pending request is cancelled, a cancellation reason (≥ 10 characters) is required, status changes to Cancelled, and an audit log entry is created And only the requester or an admin can delete their draft or cancel a pending request
Prevent Bypass via Direct API Calls
Given a client calls any sensitive action endpoint directly When the call lacks a valid Approved request ID whose stored hash matches the intended payload Then the service returns HTTP 403 with error code APPROVAL_REQUIRED and no side effects occur And all sensitive endpoints enforce server-side approval checks (no reliance on client-side logic) And all failed bypass attempts are logged with user identity (if any), IP, timestamp, and payload digest And repeated unauthorized calls are rate-limited at 10/min per user/IP with HTTP 429 on excess
Approval Routing, Quorum, and Escalation
"As an admin, I want approval requests routed to eligible approvers with automatic escalation so that decisions are timely and compliant with our governance rules."
Description

Implement a routing engine that selects eligible approvers based on policy, excludes the requester, enforces two distinct approvers (quorum), and applies conflict-of-interest rules (e.g., same team or reporting line restrictions). Support timeouts, automatic escalation to owners or backups, and delegation/out-of-office settings. Allow reassignment by admins with a logged reason. Provide SLAs and cutoff windows (e.g., end-of-day) to keep requests moving. Expected outcomes: predictable turnaround times and fair, auditable decision-making.

Acceptance Criteria
Eligibility Routing and Requester Exclusion
Given a request is submitted under policy P with eligible approver set E And requester R belongs to E And conflict-of-interest rules disallow same team and direct reporting line When the routing engine selects approvers Then the candidate set excludes R And the candidate set excludes any approver violating conflict-of-interest rules And the candidate set includes only active users And if the candidate set size is 2 or more, two distinct approvers are assigned And if the candidate set size is less than 2, the request is marked for escalation to meet quorum
Quorum Enforcement and Decision Outcomes
Given two distinct approvers A and B are assigned to a request When A approves Then the request status remains Pending second approval And A cannot submit another decision When B approves Then the request status becomes Approved and the downstream action is unblocked When any assigned approver rejects Then the request status becomes Rejected and no further approvals are accepted
Timeout and Automatic Escalation
Given policy P defines an SLA response time of S business hours and an end-of-day cutoff C And approver A is assigned and has taken no action When time since assignment reaches S or local time reaches C, whichever comes first Then the request is escalated to the next approver per policy escalation chain And A is notified of the escalation And an audit entry is created for the escalation And escalation repeats until quorum is met or the escalation chain is exhausted And if the escalation chain is exhausted without meeting quorum, the request is escalated to the policy owner or backup
Delegation and Out-of-Office Handling
Given approver A has an active out-of-office window with delegate D configured When routing assigns A during the out-of-office window Then the assignment is automatically given to D if D is eligible and not in conflict And notifications are sent to D And an audit entry records that A delegated to D When A goes out-of-office after assignment and has not acted Then the pending approval is reassigned to D under the same conditions
Admin Reassignment with Reason and Safeguards
Given an admin opens a pending approval assigned to approver A When the admin reassigns it to approver B and provides a non-empty reason Then reassignment succeeds only if B is eligible, not the requester, not in conflict, and not already assigned And the number of distinct assigned approvers remains at least two And both A and B receive notifications of the change And an audit entry captures who reassigned, from A to B, timestamp, and the reason When any eligibility or quorum rule would be violated Then the system blocks the reassignment and displays a descriptive error
SLA Tracking, ETA, and Breach Indicators
Given policy P defines SLA S and cutoff C When a request is routed Then timers start for each approver and for the overall request And the UI shows an ETA that respects business hours and cutoff windows And the request displays status badges: On track, At risk (<=20% SLA remaining), or Breached (>S elapsed) When the request is completed Then actual turnaround time is recorded for reporting
End-to-End Auditability and Export
Given any approval flow event occurs (assignment, approval, rejection, escalation, delegation, reassignment, timeout) When the event is committed Then an immutable audit record is stored with fields: request ID, event type, actor, target (if applicable), timestamp (UTC), channel, reason (if provided), pre-state, post-state And audit records are filterable by date range, event type, and actor And audit records can be exported to CSV by admins within the data retention window
Email and Slack Actionable Approvals
"As an approver, I want to approve or deny requests directly from email or Slack so that I can respond quickly wherever I am."
Description

Deliver approval requests via secure, actionable emails and Slack interactive messages with Approve/Deny options, comments, and a deep link to the full request. Use signed, expiring tokens and optional one-time codes to authenticate quick actions without full login. Provide message templates, localization, and per-organization branding. Support retries, rate limits, and error handling for provider outages. Expected outcomes: faster approvals and higher response rates without forcing approvers into the dashboard.

Acceptance Criteria
Approve via Secure Email Link
Given a pending two-key approval request is assigned to the approver and email delivery is enabled And a signed, single-use token with a 15-minute TTL is embedded in the Approve/Deny links When the approver clicks Approve within the token TTL Then the system authenticates the action using the token without full login And records the decision, timestamp, approver identity, channel=email, IP, and user agent And persists any comment provided in the email action form with the decision And invalidates the token to prevent replay; subsequent uses are rejected with a safe message And displays a confirmation page within 2 seconds including a deep link to the full request And writes an immutable audit log entry for the decision containing request ID and token ID
Deny via Slack Interactive Message
Given a pending two-key approval request is posted to the approver via a Slack interactive message And Slack request signatures are validated and the approver has permission to act When the approver taps Deny and enters a comment Then the decision is recorded as Denied with the comment, timestamp, approver identity, channel=slack And the Slack message updates to reflect the final status within 2 seconds and disables action buttons And any subsequent interaction returns an "already processed" notice without changing the outcome And an immutable audit log entry is created with Slack message ID and request ID
One-Time Code Challenge on High-Risk Approval
Given the organization has enabled OTP for quick actions on high-risk approval types And the approver clicks Approve via email or Slack When the system prompts for a 6-digit one-time code and sends the code to the approver's registered email Then the code is valid for 5 minutes, single-use, and allows up to 3 attempts And upon correct entry, the approval is completed and logged; upon failure or expiry, the decision is not applied and a safe error is shown And all OTP events (generated, verified, failed) are recorded in the audit log
Deep Link to Full Request Detail
Given an actionable message includes a deep link to the full approval request When the approver opens the link while not authenticated Then the system prompts for authentication and, after success, returns to the request detail view And if the link includes an expired/invalid token, the system shows an informative error and offers to send a fresh actionable message And access controls ensure only authorized approvers can view or act on the request
Localized, Brandable Messages
Given an organization has configured branding (logo, name, primary color) and default locale When approval emails and Slack messages are generated Then the content uses the organization's branding and locale-specific templates And required placeholders (requester, action, deadline, request ID, deep link) are populated correctly And for unsupported locales, the system falls back to English templates without template errors And emails pass rendering checks in major clients (Gmail, Outlook, Apple Mail) and Slack blocks validate without errors
Resilient Delivery with Retries and Backoff
Given the email or Slack provider returns transient errors (HTTP 5xx, network timeouts) When sending an approval message Then the system enqueues the message and retries with exponential backoff up to 5 attempts over 30 minutes And on final failure, it records the failure, surfaces an alert/metric, and provides an admin action to retry manually And once the provider recovers, only a single deduplicated message is delivered per request per channel And all delivery attempts and outcomes are captured in message logs
Rate Limits and Deduplicated Notifications
Given a single approval request generates multiple triggers within a short period When attempting to send actionable messages Then the system enforces a per-request per-approver per-channel limit of 1 message per 10 minutes And collapses duplicate messages into a single updated thread (email subject threading; Slack thread) where supported And API calls that exceed limits receive a 429 with retry-after headers And rate-limit events are logged and observable
Enforcement & Transactional Safeguards
"As a platform engineer, I want sensitive actions to execute only after verified approvals and with transactional safeguards so that system integrity is maintained under load and failure scenarios."
Description

Gate sensitive actions so they cannot execute until quorum is met. Bind approvals to the exact payload hash; if the underlying data changes, require re-approval. Enforce idempotency keys and locking to prevent double execution and race conditions. Provide compensating actions/rollback if downstream steps fail after approval. Ensure enforcement at service and API layers to prevent client-side bypass. Expected outcomes: strong guarantees that no unapproved changes can slip through and consistent system state under concurrency.

Acceptance Criteria
Quorum Gating for Sensitive Actions
Given a sensitive action requires two approvals and a business reason When an initiator submits the action Then the system records the request as Pending with status Awaiting Quorum, stores the reason, and prevents execution Given fewer than the required approvals have been recorded When the action is invoked via UI or API Then the system returns 403 APPROVAL_QUORUM_NOT_MET and performs no state changes Given the final required approval is granted within the configured validity window When approvals reach quorum Then the action transitions to ReadyToExecute and may be executed exactly once by the orchestrator Given the validity window has elapsed before execution When an execution attempt occurs Then the system blocks execution with 409 APPROVAL_EXPIRED and requires re-approval
Payload Hash Binding and Mutation Re-approval
Given an approval is created for a specific canonical payload When the payload hash is computed and stored with the approval Then the approval is cryptographically bound to that hash and exposed read-only Given any field of the underlying payload changes before execution When a hash mismatch is detected Then the approval is invalidated with status Invalidated: Payload Changed, and execution is blocked until re-approval Given an approval token is presented at execution time When the service validates token signature, payload hash, approver identities, scope, and expiry Then execution proceeds only if all validations pass; otherwise 403 APPROVAL_TOKEN_INVALID is returned and no state changes occur
Idempotency Keys Prevent Double Execution
Given an execution request includes an idempotency key scoped to tenant+action+payload-hash When the same key is received again within the configured TTL Then the system returns the original result without reapplying side effects and marks the response Idempotency-Replayed=true Given two identical requests with the same idempotency key arrive concurrently When processed by the system Then exactly one execution performs side effects and the other returns the stored response Given a retry occurs after a transient failure post-commit When retried with the same idempotency key Then the system returns the committed outcome without duplication and logs the replay
Concurrency Locking and Race Condition Prevention
Given multiple workers attempt to execute the same approved action When contending for execution Then only one worker acquires a distributed lock and proceeds; others receive 409 EXECUTION_IN_PROGRESS and perform no side effects Given the executing worker crashes after lock acquisition but before completion When the lock lease expires Then another worker can resume or retry safely without duplicate side effects Given lock acquisition exceeds the configured timeout When contention persists Then the system aborts the attempt, emits metrics/alerts, and guarantees no partial commits occurred
Compensating Actions and Rollback on Partial Failure
Given an approved action executes a multi-step workflow with downstream dependencies When a downstream step fails after one or more steps have committed Then the system triggers defined compensating actions to reach an eventually consistent target state and marks the action as Compensated Given compensation completes successfully When the action is finalized Then the audit log records all compensating steps, and stakeholders are notified with the final state summary Given compensation cannot fully restore system state When retries are exhausted Then the action is quarantined, further side effects are blocked, and an escalation alert with manual recovery runbook link is issued
Server- and API-layer Enforcement to Prevent Client Bypass
Given any service endpoint that mutates sensitive state When called without a valid approval token matching the payload hash and scope Then the service rejects the call with 403 and logs actor, IP, endpoint, and reason code Given a client attempts to bypass the UI and call internal APIs directly When the request reaches the service layer Then schema and business-rule validation deny the write without an approval token and return 403 APPROVAL_REQUIRED Given an approval token is replayed for a different payload or after expiry When validation runs Then the request is rejected, the token is flagged as replayed or expired, and a security alert is generated
Immutable Audit Trail & Reporting
"As a studio owner, I want an immutable, searchable approval log so that I can prove accountability and review any sensitive change end-to-end."
Description

Create an append-only audit log recording requester, approvers, timestamps, decisions, reasons, comments, IP/agent, payload hashes, and policy version used. Provide searchable filters, per-request timelines, and CSV/JSON export. Implement tamper-evidence via hash chaining or external timestamping, and define data retention controls. Expose summary reports (e.g., approvals by category, average time to decision) and integrate with existing ClassTap reporting. Expected outcomes: complete traceability for compliance and faster investigations of disputed changes.

Acceptance Criteria
Append-Only Audit Log Entry for Two-Key Approval Request
Given a user initiates a sensitive action that triggers a two-key approval workflow When the approval request is submitted Then the system appends a new audit entry containing: request_id, action_category, requester_id, requester_role, requested_at (UTC), requester_ip, requester_user_agent, policy_version_id, required_approvals, request_payload_hash (SHA-256), status "Pending" And the entry exposes a unique sequence_no and entry_hash (SHA-256 of canonical fields) Given any user or admin attempts to modify or delete an existing audit entry When the operation is executed via UI or API Then the system rejects the mutation with a 403 and records a mutation_denied audit event referencing the original request_id and sequence_no
Approver Decision Logging with Reason Capture (Web, Email, Slack)
Given an approver receives a decision link via web, email, or Slack When the approver submits Approve or Reject Then an audit entry is appended with: request_id, approver_id, approver_role, decision, decision_at (UTC), approver_ip, approver_user_agent or integration_id, reason (required), optional comment, and prev_hash reference to the prior entry And the system enforces reason as non-empty; if empty, the decision is rejected with 422 and no log entry is created And duplicate submissions of the same decision by the same approver within 5 minutes are idempotent and do not create duplicate audit entries
Tamper-Evidence via Hash Chaining and External Timestamping
Given the audit log contains at least one entry When a new entry n is written Then it includes prev_hash = SHA-256(entry n-1 canonical form) and entry_hash = SHA-256(entry n canonical form) And within 60 seconds of write, an external timestamp receipt is obtained and stored for the latest chain head (e.g., RFC 3161 or equivalent), including verification data Given an integrity check is initiated When the system validates all prev_hash links and external receipts across the selected range Then the result is PASS if all hashes and receipts verify, else FAIL with the index of the first failing entry and reason, and the integrity check outcome is itself logged
Search, Filter, and Export Audit Entries
Given at least 10,000 audit entries exist When a user with audit_view permission applies filters (date range, requester_id, approver_id, decision, action_category, policy_version_id, ip, status) with pagination and sorting Then only matching entries are returned, sorted as requested, along with total_count, and page metadata And filter logic is AND across fields; requester_id=A and decision=Reject returns only entries meeting both Given a user without audit_view permission When they attempt to query or export Then the system returns 403 and no data Given a filtered result set When the user exports to CSV or JSON Then the export contains only the filtered records and includes fields: request_id, sequence_no, action_category, requester_id, approver_id (if applicable), role, decision/status, reason, comment, timestamps (UTC ISO8601), ip, user_agent or integration_id, policy_version_id, request_payload_hash, prev_hash, entry_hash And CSV is UTF-8 with a header row; JSON is an array of objects; a file checksum (SHA-256) is provided and matches the downloaded file
Per-Request Timeline View with Decision Duration
Given a request_id exists with multiple related audit events When a user opens the timeline view for that request Then events are displayed in strict chronological order with exact UTC timestamps, actor identities, decisions, reasons, comments, IP/agent, and hashes And the view shows computed time_to_first_response and time_to_final_decision, where time_to_final_decision = timestamp of last required approval or final rejection minus requested_at And links to raw entries and single-request export (CSV/JSON) are available
Data Retention, Purge, and Legal Hold Controls
Given retention policies are configurable per action_category (e.g., 1 year, 3 years) When a scheduled purge job runs Then entries older than the configured window are purged, a purge_summary audit event is appended (categories, counts, time), and chain continuity is preserved via anchor entries containing last-retained prev_hash values Given a legal hold is applied to specific request_ids or categories When purge executes Then held entries are not purged, and any attempted purge of held data is denied and logged And running integrity check after purge returns PASS and indicates pruned segments without hash breaks
Summary Reports and ClassTap Reporting Integration
Given audit data exists for the last 90 days When a user opens the reporting dashboard or calls the reporting API Then the following metrics are available and accurate: approvals by category, approvals per approver, average time to first response, average time to final decision, rejection rate, top reasons And all metrics support date range and category filters, and respect RBAC permissions consistent with existing ClassTap reporting And reports can be exported (CSV/JSON) and are visible alongside existing ClassTap reports without breaking existing dashboards
Notifications, Reminders, and SLA Dashboard
"As an organization admin, I want reminders and SLA visibility so that approvals don’t stall and I can optimize our process over time."
Description

Send real-time notifications to selected approvers and watchers, with configurable reminder cadences and daily digests for pending items. Surface an SLA dashboard showing request volumes, approval times, aging, and bottlenecks by category or team. Allow threshold alerts (e.g., requests pending >24h) and per-organization notification settings. Expected outcomes: reduced approval latency and operational visibility to optimize policies and staffing.

Acceptance Criteria
Real-Time Notifications to Approvers and Watchers
Given an approval request is submitted with selected approvers and watchers and organization-level channels (email and/or Slack) are enabled When the request is submitted Then each approver receives a notification on each enabled channel within 60 seconds containing: request ID, requester, business reason, action required, due time, and approve/decline links Given watchers are assigned to the request When the request is submitted Then each watcher receives an FYI notification on each enabled channel within 60 seconds containing: request ID, requester, business reason, and current status, without approval actions Given multiple endpoints exist for a user per channel When the notification is sent Then the user receives at most one message per channel and no duplicate messages per channel are created
Configurable Reminder Cadence for Pending Approvals
Given a request remains pending, an approver has not acted, and the organization has set a reminder cadence (e.g., 2h, 24h) and maximum attempts (e.g., 5) When the cadence interval elapses Then a reminder is sent to only the pending approver(s) on enabled channels and the reminder content includes time pending and a direct action link Given an approver acts on the request or the request is canceled or expires When the next reminder would have been due Then no further reminders are sent for that request Given watchers are associated with the request When reminders are sent Then watchers do not receive reminders unless the organization setting explicitly enables watcher reminders
Daily Digest for Pending Items
Given the organization has enabled daily digests and configured a delivery time and timezone When the scheduled digest time occurs Then a single digest is delivered to configured recipients summarizing pending requests (created or still pending in the last 7 days), including counts by category/team, oldest age, and items breaching thresholds, with links to filtered views Given there are no pending items for the digest window When the scheduled digest time occurs Then no digest is sent if empty-state delivery is disabled, or an empty-state digest is sent if enabled, as per organization settings
SLA Dashboard Metrics and Filters
Given a user with reporting access opens the SLA dashboard and selects a date range When the dashboard loads Then it displays request volume, average and median approval times, p90 approval time, and aging buckets (0–24h, 24–48h, 48–72h, >72h) for the selected range Given categories and teams metadata are available When filters are applied by category or team Then all metrics and charts refresh to reflect the applied filters within 3 seconds Given approvals are completed during the selected period When the dashboard is refreshed Then metrics reflect new events within 5 minutes
Bottleneck Identification by Category or Team
Given the SLA dashboard is viewed for a selected date range When the user opens the Bottlenecks view Then categories and teams are ranked by highest median approval time and by count of items in the >24h aging bucket Given a category or team is selected from the Bottlenecks view When the user drills down Then a list of outstanding requests for that segment is shown with age, assigned approver(s), and current stage
Threshold Alerts for Aging Requests
Given the organization has configured a threshold alert rule (e.g., requests pending > X hours) with a scope (all, category, or team) and delivery channels When any request exceeds the configured threshold Then an alert is sent to the configured channels including request ID, age, category/team, and a direct link to the request Given an alert has been sent for a request and the request remains above threshold When the rule's resend interval elapses Then a repeat alert is sent until the request is approved, declined, canceled, or drops below threshold Given a threshold rule is disabled or deleted When the next alert would have been due Then no alert is sent for that rule
Per-Organization Notification Settings
Given an org admin updates notification settings (enabled channels, reminder cadence, daily digest time, threshold rules) and saves changes When a subsequent notification event occurs (submission, reminder, digest, threshold breach) Then the new settings are applied to that organization only and do not affect other organizations Given invalid configuration values are entered (e.g., cadence below minimum allowed) When the admin attempts to save Then validation errors prevent saving and indicate allowed ranges Given notification settings are updated When the change is saved Then changes take effect within 1 minute for new events

TrailDiff

Human‑readable audit timeline that records who did what, when, where (IP/device), with before/after values for roles and settings. Fast filters by user, asset, or date, CSV export for audits, and one‑tap restore to a previous role configuration to quickly undo mistakes.

Requirements

Unified Audit Event Schema & Storage
"As an owner or auditor, I want every critical action recorded in a standardized, immutable log so that I can trace changes and prove compliance."
Description

Define and implement a canonical, append-only audit event model that captures actor, action, target asset, timestamp (UTC), source (UI/API), outcome, correlation ID, and normalized before/after snapshots. Persist events in a tamper-evident, multi-tenant store partitioned by tenant and time with secondary indexes for actor_id, asset_id, action_type, and timestamp to support sub-second queries at scale. Instrument all critical ClassTap domains (scheduling, payments, invoices, roles, settings) to publish events via a durable, idempotent pipeline. Include PII tagging for selective masking/redaction, retention policies, and time zone normalization for display. Target ingestion overhead under 200ms per event and 99.9% durability.

Acceptance Criteria
Canonical Event Schema Completeness and Correctness
Given a change occurs in a critical domain (roles, settings, scheduling, payments, invoices) When the system records the audit event Then the event includes non-null actor_id, action_type, asset_type, asset_id, timestamp_utc (ISO 8601, Z), source (UI|API), outcome (SUCCESS|FAILURE), correlation_id, before_snapshot, after_snapshot And timestamp_utc is in UTC and monotonic per correlation_id And before_snapshot and after_snapshot are normalized JSON per the canonical schema with stable keys and excluded ephemeral fields And action_type and asset_type are from the approved enumerations And all events emitted by the same request/workflow share the same correlation_id
Append-Only and Tamper-Evident Storage
Given an audit event is committed When any update or delete is attempted on that event Then the system rejects the mutation and records a separate security audit of the attempt And each event stores hash and prev_hash to form a verifiable chain per tenant and chronological segment And a daily verification job recomputes the chain; any discrepancy triggers an alert and marks the segment as suspect
Multi-Tenant Partitioning and Indexed Access Paths
Given events exist for tenants A and B across 90 days When querying with tenant_id=A Then no events from tenant B are returned And storage partitions by tenant_id and time window (e.g., daily buckets) And secondary indexes exist and are populated for actor_id, asset_id, action_type, and timestamp_utc And index integrity checks report zero missing or orphaned index entries
Sub-Second Filtered Queries at Scale
Given a dataset of at least 100M events with realistic distributions When filtering by any single indexed field or by two-field combinations (e.g., actor_id + date range) Then the first page (50 items) returns in ≤800 ms p95 and ≤1200 ms p99 without cache warming And results are correct, ordered by timestamp_utc desc, with a stable pagination cursor And a count-only query over a date range returns in ≤500 ms p95
Durable, Idempotent Event Ingestion with Overhead Budget
Given the producer publishes an event with idempotency_key X and correlation_id Y When the same event is retried up to 3 times due to transient failures Then only one stored event exists for idempotency_key X and correlation_id Y And ingestion overhead (producer ack to durable commit) averages ≤100 ms and is ≤200 ms at p95 under nominal load And the system acknowledges only after durable write to primary plus at least one replica, achieving ≥99.9% durability And under simulated node failure during write, the event remains present after recovery with no duplicates
PII Tagging with Selective Masking/Redaction
Given a field in the event schema is tagged as PII When retrieving events via API/UI without elevated permission Then PII fields are masked according to policy (e.g., partial reveal or tokenized) And an authorized auditor role can request unmasked view; the access is logged as a separate audit event with correlation_id And CSV export defaults to masked PII; unmasked export requires an explicit audited override flag And PII tags propagate to before_snapshot and after_snapshot consistently
Retention Enforcement and Time Zone Normalization
Given tenant T has a 365-day retention policy When events exceed 365 days since timestamp_utc Then those events are purged and no longer retrievable via API/UI or export, and a deletion audit is recorded And purge jobs complete within the maintenance window without affecting query SLAs for active data And all displays show timestamps normalized to the viewer’s timezone preference while preserving UTC in storage and APIs may include both UTC and localized representations
Actor & Context Attribution (IP/Device)
"As a studio owner, I want to see the IP and device used for each change so that I can verify suspicious activity and enforce security policies."
Description

Capture and persist contextual metadata for each event, including authenticated user ID, role at time of action, session ID, IP address (masked per policy), user agent, device fingerprint, and approximate geolocation derived from IP. Record authentication method (password, SSO), MFA status, and request origin (web, mobile, API key). Normalize and store context in a privacy-aware format with configurable masking to meet regional compliance (e.g., GDPR). Expose context consistently in the UI and exports to support investigation and anomaly detection.

Acceptance Criteria
Write-Time Context Capture Across Origins
Given an event is generated by an authenticated actor via web, mobile, or API key When the event is persisted to TrailDiff Then the event record includes: actor_user_id, role_at_action, session_id (required for web/mobile), ip_masked, user_agent, device_fingerprint_hash (null for API key), geo_city, geo_country, auth_method, mfa_status, request_origin And ip_masked is stored masked per active policy at write time and the full IP is never stored And role_at_action reflects the role effective at the exact event timestamp, independent of later changes And request_origin is one of [web, mobile, api_key] And auth_method is one of [password, sso, api_key] And mfa_status is one of [true, false, unknown] with unknown only permitted for api_key origin And geo_city and geo_country are derived from the IP and contain no street-level or coordinate data And all fields conform to the normalized schema and enumerations defined for audit events
Regional Masking and Privacy Controls
Given the tenant’s compliance region is configured (e.g., GDPR/EU vs Non-EU) with masking levels per region When any audit event is stored Then IPv4 is masked to at least /24 and IPv6 to at least /48 for all regions And for GDPR regions, IPs are additionally hashed with a tenant-scoped rotating salt before any export And device_fingerprint is hashed with a tenant-scoped salt and never stored in raw form And geolocation is limited to city and country only; coordinates are not stored And an admin can configure mask levels per region within minimums, and any change is audit-logged as a separate event And masking is applied consistently across storage, UI, and CSV exports
UI Exposure Consistency in TrailDiff
Given a user views the TrailDiff timeline and opens any event’s detail panel When the event is rendered in the UI Then the following context fields are visible with consistent labels and formats: Role at action, Session ID, IP (masked), User agent, Device fingerprint (short hash), Geo (City, Country), Auth method, MFA status, Request origin And masked values are visually indicated and never reveal the full IP or raw device fingerprint And when request_origin=api_key, the actor is shown as the integration name/key alias (no secret), and MFA is displayed as N/A And when a field is unavailable, the UI shows “Not captured” without layout errors And timestamps are shown in the viewer’s local timezone with a UTC tooltip
CSV Export Contains Masked Context Fields
Given a user exports TrailDiff results with any combination of filters When the CSV file is generated Then it contains columns for: event_id, timestamp_utc, actor_user_id, role_at_action, session_id, ip_masked, user_agent, device_fingerprint_hash, geo_city, geo_country, auth_method, mfa_status, request_origin And all values respect current masking policy; no full IPs or raw device fingerprints appear anywhere in the file And timestamps are ISO 8601 in UTC; empty values are blank, not the string “null” And column headers are stable and column order is consistent across exports
Normalized Storage Schema and Enumerations
Given the audit event storage schema is defined When a new event is inserted Then context fields are stored with canonical names and types: actor_user_id (UUID), role_at_action (enum string), session_id (UUID, nullable for api_key origin), ip_masked (string), user_agent (string), device_fingerprint_hash (string, nullable for api_key origin), geo_city (string), geo_country (ISO 3166-1 alpha-2), auth_method (enum), mfa_status (enum), request_origin (enum) And values outside allowed enumerations are rejected and the write fails with a validation error And required fields are non-null according to origin-specific rules And event writes are idempotent by event_id to prevent duplicates
One-Tap Restore Action Attribution
Given an admin performs a one-tap restore to revert a previous role configuration When the restore completes Then a single role_restore event is persisted containing full actor and context fields (including role_at_action, session_id, ip_masked, user_agent, device_fingerprint_hash, geo_city, geo_country, auth_method, mfa_status, request_origin) And the event includes before_values and after_values for each changed role/setting And role_at_action represents the actor’s role at the time the restore was invoked And the event appears in the TrailDiff UI and CSV export with masking applied according to policy
Role & Setting Diff Computation
"As an admin, I want human-readable before/after diffs for roles and settings so that I can quickly understand what changed."
Description

Compute human-readable before/after diffs for role memberships, permission grants, and configuration settings. Normalize nested structures, arrays, and enum values to produce concise, meaningful change summaries (added/removed/modified). Store both the rendered summary and a machine-readable patch to enable future replays and restores. Handle schema evolution and migrations gracefully by versioning diff formats. Surface diffs consistently in event detail views and exports.

Acceptance Criteria
User Role Membership Added and Removed Diff
Given a user with roles ["Instructor"] in organization "Studio A" When their roles are updated to ["Instructor", "Admin"] Then the summary includes "Role added: Admin" and includes no other role changes And the machine patch contains exactly one add operation targeting the Admin membership for that user and organization Given the same user now has roles ["Admin"] When the update is saved Then the summary includes "Role removed: Instructor" And the patch contains exactly one remove operation for the Instructor membership Given roles are saved in any order with identical membership sets When the change is committed Then no role-related diff is produced
Permission Grants Modified Across Multiple Assets
Given permissions ManageClasses=false and ViewRoster=true on assets Class:123 and Class:456 When ManageClasses is set to true on Class:123 and ViewRoster is set to false on Class:456 Then the summary groups changes by asset and lists "Permission modified: ManageClasses false→true" under Class:123 and "Permission modified: ViewRoster true→false" under Class:456 And the machine patch contains two replace operations with asset-scoped targets Given a permission is toggled and toggled back before save When the change is committed Then no permission diff is stored
Normalized Diff for Nested Settings, Arrays, and Enums
Given settings { cancellationPolicy.windowHours: 24, waitlist.mode: "FIFO", waitlist.rules: [{ id: "r1", minutesBefore: 30 }, { id: "r2", minutesBefore: 5 }] } When updated to { cancellationPolicy.windowHours: 12, waitlist.mode: "PRIORITY", waitlist.rules: [{ id: "r2", minutesBefore: 5 }, { id: "r1", minutesBefore: 45 }] } Then the summary shows exactly: - Modified: cancellationPolicy.windowHours 24→12 - Modified: waitlist.mode FIFO→PRIORITY - Modified: waitlist.rules[r1].minutesBefore 30→45 And shows no diff for array reordering And enums are rendered using display labels consistently across UI and export And the patch uses stable identifiers (id) for array members and contains no operations caused solely by order changes Given null and undefined are both considered absent for optional fields When a value transitions from unset to null (or null to unset) Then no diff is emitted
Atomic Storage of Rendered Summary and Machine-Readable Patch
Given a change produces a diff When the event is written Then the rendered summary and machine-readable patch are stored atomically under the same eventId with a diffFormatVersion present And both are retrievable via API within 100 ms P95 for a dataset of 1,000 events Given a partial write failure occurs When retry logic executes Then either both summary and patch are present exactly once (idempotent) or neither is stored, and an error with correlationId is logged Given a stored event When exported via CSV Then the summary lines match exactly the stored rendered summary for that event
Diff Format Versioning and Migration Compatibility
Given diffFormatVersion=2 is current and events exist with diffFormatVersion=1 When reading any event Then the system returns a normalized representation to the consumer and preserves the original version in metadata Given a migration renames a setting path (e.g., waitlist.mode→queue.mode) When a v1 diff referencing the old path is replayed on the migrated schema Then a migration map translates the old path, and the patch applies without error Given an unknown future diffFormatVersion is encountered by a down-level reader When processing events Then the event is skipped with a clear "Unsupported diff version" error and no partial application occurs
One-Tap Restore Applies Patch Safely
Given a prior diff event with a stored patch When the operator selects Restore Then the system dry-runs the patch, detects conflicts, and presents a summary of intended changes and any conflicts When the operator confirms apply Then 100% of non-conflicting operations are applied, changes are persisted, and a new diff event is recorded with type=Restore referencing the source eventId And conflicting operations are not applied, and the restore result lists them explicitly Given the restore completes When the state is compared to the source event Then the post-restore state matches for all successfully applied paths, verified by recomputing a zero-diff against the source
Consistent Diff Surfacing in Event Detail View and CSV Export
Given an event with N summary lines When viewing the event detail Then exactly N lines are shown in the Diff section in the same order and wording as stored Given a CSV export of events filtered by user, asset, and date range When opened Then each row contains columns: eventId, timestamp, actor, scope, diffFormatVersion, summaryLine, patchRef And the number of rows equals the sum of summary lines across the exported events Given the same event is viewed in UI and exported to CSV When compared Then there is a 100% match of summaryLine text and field values for eventId, actor, and scope
Timeline View with Fast Filters & Search
"As an admin, I want a fast, filterable timeline of events so that I can locate specific changes without sifting through noise."
Description

Deliver a responsive, paginated audit timeline UI with instant filters by user, asset, action type, outcome, and date range, plus keyword search across event summaries and metadata. Support deep linking and saved filter presets, display times in the viewer’s local time zone, and provide infinite scroll with cursor-based pagination. Include accessible UI components, empty/error states, loading skeletons, and copy-to-clipboard for event IDs. Target P95 filter response under 500ms on datasets up to 5M events per tenant.

Acceptance Criteria
Instant Multi-Filter Response (User/Asset/Action/Outcome/Date)
- Given a tenant with up to 5,000,000 events, When the viewer applies any combination of filters (user, asset, action type, outcome, date range ≤ 365 days), Then results update with P95 response time ≤ 500 ms over 1,000 consecutive filter requests and only matching events are shown. - Given one or more filters applied, When the viewer removes a single filter chip or taps Reset, Then results reflect the change within P95 ≤ 500 ms and remaining filters persist. - Given an invalid filter value or date range (start > end), When applied, Then a validation message is shown inline and no query is sent until corrected.
Keyword Search Across Summaries and Metadata
- Given a search term of 2–64 characters, When submitted, Then results include events where the term matches event summaries or metadata fields (event_id, IP, device, role, asset name) using case-insensitive substring matching. - Given filters and a search term, When executed together, Then the returned set is the intersection and P95 response time remains ≤ 500 ms for datasets up to 5,000,000 events. - Given no matches, When search completes, Then show an empty state including the term and a Clear Search control; clearing returns to the previous result set within P95 ≤ 500 ms.
Deep Linking and Saved Filter Presets
- Given any filters and/or search term applied, When the viewer copies or shares the page URL, Then URL parameters encode the current state (filters, search, sort, cursor) and opening the URL restores the same state and results. - Given the viewer saves a named preset, When selected later, Then the exact filter/search combination is applied, reflected in the URL, and a confirmation toast appears; saving a preset with an existing name prompts to overwrite. - Given a preset is deleted, When confirmed, Then it is removed from the list; visiting an old preset URL shows “Preset not found” while retaining the underlying filter parameters in the view.
Infinite Scroll with Cursor-Based Pagination
- Given an initial page of results, When the viewer scrolls past 90% of the list, Then the next page is fetched using an opaque cursor and appended without duplicates or gaps; page size adapts between 25–100 items based on viewport height. - Given the next-page request fails, When the error occurs, Then a non-blocking error message with Retry appears and previously loaded pages remain visible; retrying resumes from the last valid cursor. - Given new events arrive while the user is scrolled, When detected, Then a “New events available” indicator appears; activating it prepends new items and maintains the user’s relative scroll position.
Local Time Zone Rendering
- Given the viewer’s device time zone, When rendering event timestamps, Then times display in local time with explicit zone abbreviation (e.g., PDT) and an ISO 8601 tooltip including UTC offset. - Given a deep-linked date range, When opened in a different time zone, Then the start/end instants are preserved in UTC and rendered correctly in the local zone boundaries. - Given assistive technologies, When announcing timestamps, Then screen readers read full date, time, and time zone in a human-friendly format.
Accessibility, Keyboard Navigation, and Responsive Layout
- Given a keyboard-only user, When navigating filters, search, presets, and timeline items, Then all controls are reachable in a logical tab order, have visible focus, and operate via Enter/Space/Escape; focus is trapped in modals/drawers. - Given screen reader users, When interacting, Then all controls have roles, names, and states; loading uses polite live regions; color contrast and touch targets meet WCAG 2.1 AA and min 44x44 px. - Given various viewports (320–599, 600–899, 900–1199, ≥1200 px), When viewing the timeline, Then no horizontal scroll occurs; the filter panel behaves as drawer/tablet side panel/desktop sidebar respectively; primary actions remain visible.
Empty/Error States, Loading Skeletons, and Copy Event ID
- Given zero matching results, When a query returns empty, Then show an empty state with guidance and a one-click Clear All action; no skeletons are shown after the empty state renders. - Given data is loading (initial load, filter change, pagination), When fetch is in-flight, Then display skeleton rows that preserve layout and avoid layout shift (CLS ≤ 0.1). - Given an event item, When the user copies its event ID, Then the ID is placed on the clipboard with a success toast; if the Clipboard API is unavailable, a fallback select-and-copy flow is provided.
CSV Export with Timezone & Redaction
"As a compliance officer, I want to export filtered audit logs to CSV with proper time zones and redaction so that I can satisfy audit requests safely."
Description

Enable CSV export of audit events with applied filters and selected columns, generated via an async job that streams results to avoid timeouts. Offer time zone selection for timestamps, optional PII redaction based on viewer permissions, and signed, expiring download URLs. Include an export request log with requester, time, filter criteria, row count, and file hash for chain-of-custody. Enforce row limits and pagination for very large exports and send completion notifications via email/in-app.

Acceptance Criteria
Filtered Export with Selected Columns
Given a user with permission to export audit events And active filters by date range, user, and asset are applied And the user selects a specific set and order of columns When the user requests a CSV export Then the exported CSV contains only rows that match the active filters And the CSV includes only the selected columns in the selected order with a header row And all values are UTF-8 encoded and CSV-escaped per RFC 4180 And the export job is queued asynchronously and returns a job ID within 2 seconds
Time Zone Selection for Timestamps
Given a default organization time zone is configured And the user selects an IANA time zone for the export When the CSV is generated Then all timestamp fields in the CSV are converted to the selected time zone And timestamps are formatted as ISO 8601 (YYYY-MM-DDThh:mm:ssZZ) And no timestamps in the file use mixed time zones And daylight saving time transitions are correctly applied
Permission-Based PII Redaction
Given a user without PII_VIEW permission requests an export When the CSV is generated Then PII fields (email, phone, IP/device identifiers) are replaced with [REDACTED] And non-PII fields remain unmodified And the redaction state (enabled/disabled) is recorded with the export job And a user with PII_VIEW permission exporting the same data receives unredacted PII
Async Export, Streaming, and Large Dataset Pagination
Given a configured maximum rows per file MAX_ROWS and a stable sort order by (timestamp, event_id) And an export result set size exceeds MAX_ROWS When the user requests the export Then the system produces page 1 containing at most MAX_ROWS rows and a next-page token And subsequent pages can be requested using the next-page token to retrieve the next MAX_ROWS rows until complete And each page’s CSV preserves the stable sort order across pages with no duplicates or gaps And the export generation runs as an asynchronous job; the initial request returns immediately with a job ID and page context And no HTTP request to generate or download a page times out due to server-side processing
Signed, Expiring Download URL Access Control
Given an export job completes successfully When the requester downloads the file via a signed URL Then the URL is valid only for the configured TTL and embeds an unguessable signature And any tampering with URL path or query invalidates the signature and returns HTTP 403 And after expiration the URL returns HTTP 410 or 403 and no data is leaked And only the requester or users with EXPORT_ADMIN permission can obtain or use the URL And an admin can revoke the URL prior to expiration, after which downloads are blocked
Export Request Log and Chain-of-Custody
Given an export is requested When the job completes (success or failure) Then an immutable log entry is recorded containing requester ID, requested-at UTC timestamp, filters summary, selected columns, chosen time zone, redaction state, job ID, status, completed-at timestamp, result row count, file size, and SHA-256 file hash (for successes) And the file’s SHA-256 computed by re-downloading the CSV matches the stored hash And the log is queryable by date range and requester and returns accurate results And any updates to log metadata are prohibited (append-only with audit trail)
Completion Notifications Email and In-App
Given notification preferences permit export notifications And a user has requested an export When the job completes successfully Then the user receives an in-app notification and an email within 1 minute containing job ID, filters summary, time zone, redaction state, row count, and a link to the signed URL And when the job fails, the notifications include the error reason and no download link And duplicate notifications are not sent for the same job status And the in-app notification deep-links to the export history page for that job
One-Tap Restore of Role Configuration
"As an admin, I want to restore a previous role configuration with one tap so that I can quickly undo mistakes without manual rework."
Description

Provide a safeguarded restore action on role-related events that re-applies a prior role/permission state. Show a preview of changes to be applied, validate current constraints (e.g., users no longer exist, conflicting assignments), and require appropriate authorization and confirmation. Execute restores as idempotent background jobs, emit a new audit event for the restore, and support rollback on partial failure. Respect scope boundaries (studio/location) and block restores that would escalate the requester’s own permissions without secondary approval.

Acceptance Criteria
Preview of Restore Changes
Given a role-related audit event with a prior role/permission state exists When the authorized user taps "Restore" on that event Then the system displays a preview of changes showing before/after values for roles, permissions, and affected users And the preview shows the total counts of items to change and any items that are non-restorable And the Confirm action is disabled until validation completes and the user explicitly confirms the action
Authorization and Secondary Approval
Given a user attempts a restore within a studio/location scope When the user lacks the "RestoreRoleConfig" permission for that scope Then the restore action is denied with an authorization error and no job is enqueued And an audit event "restore_denied" is recorded with reason "unauthorized" Given the requested changes would escalate the requester's own permissions within the scope When the user initiates the restore Then the request enters "Pending Secondary Approval" and cannot execute And a user with Admin privileges in the same scope (not the requester) can approve or reject And only upon approval is the restore job enqueued; upon rejection, no changes occur and an audit event records the rejection
Constraint Validation Before Restore
Given the preview of a restore is generated When validation runs against the current system state Then missing users, deactivated accounts, and conflicting role assignments are listed as blocking errors And the Confirm action remains disabled while any blocking errors exist; no job can be started And non-blocking warnings (e.g., redundant permissions) are displayed but do not block confirmation
Idempotent Background Restore Job
Given an approved and confirmed restore request When the user submits the restore Then a background job is enqueued with an idempotency key derived from the source audit event ID and scope And duplicate submissions with the same idempotency key return a no-op response and do not enqueue another job And the job acquires a per-scope lock to prevent concurrent restores in the same scope And the job processes to one of the terminal states: succeeded, failed, or rolled_back, after queued and running states
Audit Event Emission on Restore
Given a restore is requested, executed, or fails When each phase occurs Then a corresponding audit event is emitted: restore_requested, restore_applied, restore_failed or restore_rolled_back And each event records actor ID, timestamp, IP/device, scope, source audit event ID, before/after diff applied, job ID, and optional reason And the events appear in the TrailDiff timeline and are available in CSV export
Scope Boundary Enforcement
Given a restore refers to changes in a specific studio/location scope When the restore job runs Then only entities within that scope are modified And any out-of-scope entities are ignored and listed as warnings And if the requester lacks access to the scope, the operation is denied and no changes occur
Partial Failure Rollback and Notification
Given an error occurs while applying any change during the restore When the job encounters the error Then all applied changes are rolled back to the pre-restore state and the job status is set to rolled_back And the requester is notified in the UI and via email with an error summary and guidance to retry And an audit event "restore_rolled_back" is recorded with error details and zero net changes
Audit Access Controls & Permissioning
"As an owner, I want strict permissions around who can view, export, or restore audit data so that sensitive information remains protected."
Description

Introduce granular RBAC for audit features, including view, filter, export, and restore capabilities. Restrict sensitive fields (e.g., partial IP, device fingerprint) to privileged roles and require MFA for export/restore actions. Enforce tenant scoping, rate limiting, IP allowlists for export endpoints, and comprehensive access logging. Provide a dedicated ‘Audit Viewer’ role template and administrative controls to delegate access safely.

Acceptance Criteria
RBAC: View and Filter Audit Events Within Tenant
Given a signed-in user with role 'Audit Viewer' or 'Tenant Admin' in tenant T When the user opens the TrailDiff audit timeline UI or calls GET /audit/events Then the request succeeds with 200 and returns only events scoped to tenant T And filter controls for user, asset, and date range are visible and functional And applying a filter yields only matching events within tenant T Given a signed-in user without the 'view_audit' permission When the user attempts to access the audit timeline or GET /audit/events Then the request is denied with 403 and no events are returned And attempting to query an event belonging to another tenant by ID returns 404 or 403
Sensitive Fields Visibility Control (IP/Device Fingerprint)
Given a user with the 'view_sensitive_audit_fields' permission When viewing audit events in UI or via API Then IP addresses are displayed in masked form (e.g., last octet obfuscated) and device fingerprints in hashed/truncated form And CSV export includes only masked sensitive fields Given a user without the 'view_sensitive_audit_fields' permission When viewing audit events in UI or via API Then sensitive columns are hidden in UI and returned as null/omitted in API and CSV And searches cannot be performed against the full underlying sensitive values
Step-up MFA Required for Export and Restore
Given a user with 'export_audit' permission and no active step-up MFA session When the user initiates a CSV export Then the user is prompted for MFA and export does not proceed until successfully completed And upon successful MFA, export completes and the file downloads with 200 Given a user with 'restore_role_config' permission and no active step-up MFA session When the user attempts a one-tap restore to a previous role configuration Then an MFA challenge is required and the restore is blocked until MFA succeeds And MFA failures result in no changes applied and a 401/403 response
Export Endpoint Protected by Tenant IP Allowlist
Given tenant T has an export IP allowlist configured (supports IPv4/IPv6 CIDR) When an export request originates from an IP within the allowlist Then the request proceeds (subject to other checks) and returns 200 When the export request originates from an IP outside the allowlist Then the request is denied with 403 and a message indicating the IP is not allowlisted And the decision is independent per tenant configuration
Rate Limiting for Audit Views/Filters and Exports
Given a per-user and per-tenant rate limit is configured for GET /audit/events When a user exceeds the configured threshold within the time window Then subsequent requests return 429 with a Retry-After header And requests succeed again after the window elapses Given a per-user and per-tenant rate limit is configured for POST /audit/exports When the user exceeds the configured export threshold Then further export attempts return 429 with a Retry-After header
Comprehensive Access Logging for Audit Feature
Given any action on TrailDiff audit features (view, filter, export, restore, permission change) When the action occurs Then an access log entry is written within 2 seconds containing actor_id, actor_roles, tenant_id, timestamp (UTC), source IP (masked per policy), device fingerprint (hashed), action_type, resource identifiers, and before/after values for role/setting changes where applicable And access logs are append-only; update/delete of log entries is not permitted (attempts return 403/405) And access logs are visible only to users with 'view_audit' and 'view_sensitive_audit_fields' as per field-level masking rules
Audit Viewer Role Template and Safe Delegation
Given the system provides a predefined 'Audit Viewer' role template with least-privilege permissions (view/filter only, no export/restore, no sensitive fields) When a Tenant Admin assigns the 'Audit Viewer' role to a user in tenant T Then the user can access audit views/filters for tenant T only and cannot export/restore or view sensitive fields And the Tenant Admin cannot grant permissions they themselves do not possess And all role assignment changes are recorded in the audit timeline with before/after values

Role Simulator

Preview a user’s exact access before saving changes. Run “Can X do Y on Z?” checks and see the interface as that role would, reducing support tickets and preventing over‑permissioning or accidental lockouts.

Requirements

Instant Role Preview Sandbox
"As an admin, I want to preview the app as a selected role before saving changes so that I can validate access without risking real user lockouts."
Description

Provide a one-click sandbox that renders ClassTap exactly as a selected user or role would see it—without persisting changes. The simulator mirrors navigation, feature flags, resource scoping (studio, class, invoice, waitlist), and time-based permissions, including mobile and web views and tenant-specific branding. All requests execute under a simulated principal with read-only semantics and no external side effects. Deep links, back/forward navigation, and component-level fallbacks are supported. The sandbox integrates with the existing RBAC/ABAC policy layer and content APIs, masking PII where configured. Performance targets: initial load under 2 seconds p95 and route transitions under 500 ms p95.

Acceptance Criteria
One-Click Sandbox Launch and Exit
Given an admin with simulate permissions selects a target user or role When the admin clicks “Preview as” Then the application enters sandbox mode in a single action, displays a persistent simulation banner with target identity and “Read-Only Simulation” tag, and preserves the current route And a device toggle allows switching between Web and Mobile views without changing data or permissions, only layout And when the admin clicks “Exit Simulation” or uses the exit control Then the session returns to the prior admin context and all simulation state (tokens, time override, device view) is cleared
Read-Only Execution and No External Side Effects
Given the simulator is active When any write operation is attempted (create/update/delete, payments, emails, webhooks, exports that would include raw PII) Then the backend evaluates authorization under the simulated principal but performs a dry-run, makes no database mutations, sends no messages/webhooks, triggers no third-party calls, and records only SimulationAccess logs And any UI action that would write shows a “Read-Only in Simulation” notice and returns a non-mutating result code And system metrics show zero committed write transactions for the session
Policy Parity, Feature Flags, and Resource Scoping
Given a target user U with defined RBAC/ABAC policies, feature flags, and resource scopes When running “Can X do Y on Z?” checks and navigating lists/details across studios, classes, invoices, and waitlists Then simulator authorization decisions and visible actions exactly match those produced for U in production And only in-scope resources for U are listed and are accessible; out-of-scope resources are hidden or return 403 consistently And feature flags for U are applied identically, enabling/disabling UI components and routes accordingly
Time-Based Permission Preview
Given the simulator shows an as-of time control defaulting to current time When the admin adjusts the preview time to a past or future moment Then effective permissions and visibility recompute to reflect time-based rules (e.g., registration windows, financial locks) and the UI updates without full reload And when the preview time crosses a boundary, gated actions/menus appear or disappear accordingly And resetting the time control to “Now” restores current-time permissions
Deep Links, Navigation Preservation, and Fallbacks
Given a simulation session is active When opening a deep link to a protected route, including directly loading a URL or using back/forward navigation Then the simulated principal and any time override remain in effect and the route renders if permitted And if permission is denied, the page renders the authorized fallback component (e.g., masked card/placeholder) without crashes or redirect loops And sharing a simulation URL does not transfer the session; recipients must start their own authorized simulation
PII Masking and Tenant Branding
Given tenant-specific branding and masking configuration are defined When entering the simulator for a user in that tenant Then tenant logo, colors, and theme render exactly as in production on all pages and device views And fields marked as PII are masked/redacted per configuration across lists, details, search results, and exports And unmasking is only available to admins with explicit Unmask permission, is scoped to the session, and is logged as SimulationUnmask
Performance SLAs in Simulator
Given real-user monitoring is enabled for simulator sessions When measuring initial sandbox entry across production-like traffic Then p95 Time-to-Interactive for the first simulation load is ≤ 2000 ms And p95 SPA route transition time within the simulator is ≤ 500 ms And simulator overhead versus a normal session increases p95 times by no more than 10%
“Can X do Y on Z?” Query Engine
"As a studio owner, I want to ask whether a specific instructor can edit a given class so that I can confidently adjust permissions before classes start."
Description

Implement a deterministic permission check service that answers actor/action/resource queries (e.g., Can Instructor A edit Class B at Time T?) with allow/deny plus a human-readable rationale (matching policies, constraints, and feature flags). Support scopes across tenants, studios, classes, invoices, payouts, and waitlists, with optional time windows and resource attributes. Provide a UI for natural-language prompts and a structured API for developer tooling, with batch queries for bulk validation. Include impact analysis suggesting the minimal policy change to grant or revoke access and surface conflicting rules. Operates against a cached policy snapshot for low latency with audit-synced refresh.

Acceptance Criteria
Deterministic allow/deny with rationale for single query
Given a cached policy snapshot version N and an (actor, action, resource, optional time) tuple When the same query is executed 100 times concurrently against the query engine Then every response is identical in structure and content and the decision is deterministic And the response includes: decision in {allow, deny}, rationale.message (non-empty, <=500 chars), rationale.policies (array of policyIds), rationale.constraints (array), featureFlagsEvaluated (array), evaluationOrder (array of ruleIds), evaluatedAt (ISO-8601), snapshotVersion=N And if no policy matches, decision=deny and rationale.message contains "no matching policy" And if multiple rules match with conflicting effects, the decision follows documented precedence and rationale lists all considered rules in evaluationOrder
Scope coverage across tenants, studios, classes, invoices, payouts, waitlists
Given policies defined across resource types tenant, studio, class, invoice, payout, waitlist When the engine evaluates representative allow and deny cases for each resource type Then for each type there exists at least one allow and one deny outcome with correct rationale.policies referencing the governing policy IDs And cross-tenant isolation is enforced: Given actor from tenant A and resource in tenant B without an explicit cross-tenant policy, Then decision=deny and rationale.message includes "cross-tenant isolation" And when an explicit cross-tenant policy exists with proper scope, Then decision=allow and rationale.policies includes that policy ID And resource scoping (e.g., studio->class) is respected per policy definitions and is explained in rationale.constraints
Time window and resource attribute constraints
Given a policy that allows edit on Class B only between 14:00–16:00 UTC When the query time is 15:00 UTC Then decision=allow and rationale.constraints includes a satisfied timeWindow with exact bounds When the query time is 17:00 UTC Then decision=deny and rationale.message mentions time window not satisfied Given a policy requiring class.status=published and capacityRemaining>0 When resource attributes match (published, capacityRemaining=5) Then decision aligns with allow and rationale.constraints lists satisfied attribute checks When attributes do not match (draft or capacityRemaining=0) Then decision=deny and rationale.constraints lists failed checks with attribute keys and expected vs actual values
Natural-language UI prompt mapping and result display
Given a user enters "Can Instructor A edit Class B at 15:00 UTC?" into the UI prompt When parsing completes Then the UI displays parsed tokens for actor=Instructor A, action=edit, resource=Class B, time=2025-09-07T15:00:00Z for confirmation And on confirm, the result panel shows decision and a human-readable rationale within 1 second p95 And if multiple entities match "Instructor A" or "Class B", a disambiguation control lists candidates with IDs and tenant/studio context and requires selection before execution And if time is omitted, the UI defaults to the studio timezone and displays the assumed time context And input validation errors render inline with actionable messages without executing a query
Structured API endpoints with batch query support
Given the POST /v1/permissions:check endpoint with body { query: { actorId, action, resource: { type, id }, time? }, includeImpactAnalysis? } When a valid request is sent with authentication Then HTTP 200 is returned with body { decision, rationale, snapshotVersion } and p95 latency <=150ms under nominal load When authentication is missing or invalid Then HTTP 401 is returned and no decision is computed Given the POST /v1/permissions:batch endpoint with body { queries: [ { id, actorId, action, resource:{type,id}, time? } ], includeImpactAnalysis? } When up to 500 queries are submitted Then HTTP 200 is returned with per-item results preserving input id; invalid items return an error object without failing the entire batch; batch p95 latency <=800ms And API schemas are validated; unknown fields are ignored and reported in a warnings array
Impact analysis with minimal policy change and conflict surfacing
Given a query that yields decision=deny When includeImpactAnalysis=true Then the response contains impactAnalysis.grant with a suggested minimal-change policy (scope, action, condition) and target policyId or a newPolicy stub, along with a justification string and estimated blastRadius in {low, medium, high} Given a query that yields decision=allow When includeImpactAnalysis=true Then the response contains impactAnalysis.revoke with a suggested minimal-change modification to revoke access with least privilege impact And if conflicting rules (allow and deny) match, response.conflicts lists the ruleIds, their effects, and the precedence rule applied; rationale references these conflicts And suggestions are reproducible: applying the grant suggestion results in allow; applying the revoke suggestion results in deny in an isolated test environment
Cached policy snapshot, latency, and audit-synced refresh
Given the engine operates on a cached policy snapshot with version N When queries are evaluated Then all responses include snapshotVersion=N and single-query p95 latency <=150ms When an audited policy change is committed Then a refresh is triggered and a new snapshotVersion N+1 is available to evaluators within 5 seconds, with zero partial-policy exposure (all-or-nothing swap) And until refresh completes, evaluators continue using version N (stale-while-revalidate) and record snapshotVersion in responses And the system exposes a health/metrics endpoint reporting current snapshotVersion, lastRefreshAt, and p95 latencies for single and batch operations
Draft Changes Preview and Permission Diff
"As a manager, I want to see how my unsaved permission edits will change what a user can do so that I avoid over-permissioning or accidental lockouts."
Description

Allow admins to stage role and policy edits and preview the exact effects before saving. Present a side-by-side diff of added/removed capabilities across resources (classes, schedules, invoices, attendees, discount codes) and highlight risky changes such as over-permissioning or potential lockouts. Provide guided recommendations to achieve least privilege, including rule templates and scope narrowing. Integrate with the simulator to render the before/after UI and with validation hooks to block saving when critical access is lost (e.g., no remaining owners).

Acceptance Criteria
Draft Mode and Non-Destructive Preview
Given an admin with Manage Roles permission When the admin edits role or policy settings in Draft mode Then the changes are stored as a draft and do not modify live permissions And the interface provides a "Preview Draft" action And selecting "Preview Draft" renders the draft's effective permissions And the system displays a banner indicating "Draft Preview — no live changes"
Side-by-Side Permission Diff Across Resources
Given a draft exists for a selected role or policy set When the admin opens the Permission Diff view Then two columns labeled Current and Draft are shown And for each resource (classes, schedules, invoices, attendees, discount codes) the capabilities added, removed, and unchanged are listed And added items are highlighted as Added, removed as Removed, unchanged as Unchanged And summary counts per resource and overall totals are displayed And the view supports filtering by resource and change type
Risk Detection and Lockout Prevention
Given the system has computed the permission diff When an over-permissioning pattern is detected (e.g., wildcard scope, delete_* across all resources, resource-wide admin) Then a risk panel surfaces each risky change with severity and rationale And risky changes are visually highlighted with suggested remedies And if the change would result in no remaining owners/admins, the Save action is blocked and an error explains the lockout risk
Guided Least-Privilege Recommendations
Given risky or broad permissions are identified in the draft When the admin opens Recommendations Then the system proposes rule templates and scope narrowing (e.g., limit to specific classes, schedules, locations, or time ranges) And selecting a recommendation applies it to the draft and updates the diff immediately And each recommendation shows the resulting capability changes before application
Simulator Integration and Can-Do Checks
Given a draft and a target role or user are selected When the admin runs a "Can X do Y on Z?" query Then the system returns Yes or No with the matched/denied policy rule explanation And toggling between Current and Draft updates the result accordingly And the simulator renders the target UI as that role would see it without enabling real data changes And query results are returned within 2 seconds for up to 1,000 permission rules
Pre-Save Validation and Safe Apply
Given a draft with pending changes When the admin clicks Apply Changes Then pre-save validation runs for critical access loss and conflicts And if validation passes, the draft is versioned and promoted to live and an audit entry records actor, timestamp, and diff summary And if validation fails, no changes are applied and detailed errors with resolution steps (e.g., assign at least one owner) are shown
Safe Simulation Guardrails and PII Masking
"As a support agent, I want simulated views to be read-only and clearly labeled so that I don’t accidentally modify live data while troubleshooting."
Description

Enforce read-only behavior in simulation mode by intercepting mutating operations (create/update/delete, payments, notifications, integrations) and substituting no-op or scrubbed responses. Clearly label all simulated screens and components, disable irreversible actions, and provide realistic but anonymized data where needed. Support configurable PII masking for attendee contact details and payment instruments to meet privacy requirements while enabling support and training workflows. Ensure third-party webhooks and emails are never triggered during simulation.

Acceptance Criteria
Enforce Read-Only Across UI and API During Simulation
Given simulation mode is active for a user impersonating role R When the user attempts to create, update, or delete any entity (classes, schedules, invoices, attendees, settings) via the UI Then all irreversible controls are disabled with a tooltip "Disabled in simulation" and no database writes occur And any attempted write API request is intercepted and converted to a no-op response with HTTP 200, header "X-Simulated: true", and body { "simulated": true, "operation": "<name>" } And the audit trail records a non-persistent entry flagged isSimulated=true and no real audit side-effects are emitted And page analytics do not record write success events (only simulated)
Block Payments and Refunds; Return Scrubbed Responses
Given simulation mode is active When a user initiates a charge, refund, capture, void, or setup intent via UI or API Then no network request is sent to any external payment gateway (0 outbound calls recorded) And a simulated gateway response is returned within 300–800 ms with realistic fields (ids prefixed "sim_", status default "succeeded") and masked instrument details (brand, last4, exp_month, exp_year only) And no ledger, balance, or payout records are created or mutated And no payment receipts, invoices, or confirmation messages are dispatched
Suppress Notifications, Webhooks, and Integrations
Given simulation mode is active When an action would trigger email, SMS, push, or third‑party webhooks/integrations Then no outbound messages or HTTP requests are sent (providers receive 0 requests) And the would-be payloads are captured in an internal Simulator Log UI with timestamp, destination, and redacted content And the outbound job queue remains empty and contains no scheduled jobs And all related UI surfaces display a badge "Notifications suppressed in simulation"
Configurable PII Masking Levels
Given org-level simulation PII masking is set to "Standard" or "Strict" When viewing attendee contact details or payment instruments in simulation Then fields are masked per level: - Standard: email "jane.doe@example.com" -> "j****@example.com"; phone "+1-415-555-2671" -> "+*-***-***-2671"; card "4242 4242 4242 4242" -> "•••• •••• •••• 4242" - Strict: email -> "*****@example.com"; phone -> "+*-***-***-****"; card -> "•••• •••• •••• ••••" with last4 only shown in tooltip for org owners And CSV/PDF exports and API GET responses in simulation return masked values only And changing the masking level takes effect within the current session on next view refresh (<2 seconds) and is audited as a simulated preference change And only org owners can change masking level; other roles see the control disabled
Global Simulation Labeling and Disabled Irreversible Actions
Given simulation mode is active across the application and embedded widgets Then a persistent top banner with text "Simulation Mode — Read Only" is visible on 100% of pages and iframes, includes an "Exit Simulation" control, and meets WCAG AA contrast And the page title prefix "[SIM]" and a distinct simulator favicon are applied And buttons/menus for irreversible actions (Delete, Archive, Publish, Send, Payout) are disabled and show a tooltip "Disabled in simulation" And any modal or confirmation dialog includes a subtitle "This is a simulation; no changes will be saved"
Realistic Anonymized Data Substitution
Given real records contain PII When rendering data in simulation Then names, emails, phones, and addresses are replaced with realistic anonymized values using reserved domains (example.com/.org/.net) and valid test number ranges And identifiers and reference codes preserve format/length constraints while being non-real (e.g., "CL-2025-000123" -> "CL-2025-9X0123") And anonymized values are deterministic per entity for the duration of the session so the same record shows consistent values across pages And the original unmasked values do not appear in the DOM, network responses, exports, or client storage And additional render time overhead from anonymization is <50 ms per page
Simulation Session Lifecycle and State Reversion
Given a user starts a simulation session Then a simulator session id is created with a 30-minute inactivity timeout When the session expires or the user clicks "Exit Simulation" Then impersonation ends, simulator flags are cleared, and the UI returns to the user's own role within 3 seconds And all simulator toggles revert to defaults; no simulated state persists between sessions And any open tabs exit simulation within 5 seconds of expiry via broadcast channel And a toast "Exited simulation" is displayed and the event is recorded in a non-persistent simulator log
Audit Trail and Shareable Simulation Snapshots
"As a compliance admin, I want an audit trail of role simulations and shareable snapshots so that we can review and approve access changes."
Description

Capture structured logs of each simulation session and permission query, including who initiated it, target user/role, scope, time, and results. Allow creation of time-limited, tenant-isolated snapshot links that reproduce the simulated state for reviewers without granting broad access. Store logs and snapshots for 30 days by default with export to CSV/JSON and SIEM integration. Redact sensitive fields according to tenant policy. Provide admin reports on usage to measure reductions in support tickets and permission errors.

Acceptance Criteria
Structured Audit Log for Simulations and Permission Queries
Given a tenant user initiates a simulation session or a "Can X do Y on Z?" permission query When the action completes Then an immutable audit record is persisted before the response is returned with fields: tenant_id, initiator_user_id, initiator_role, action_type (simulation|permission_query), target_user_or_role, scope (resource identifiers and environment), utc_timestamp, result (allow|deny with rationale), and correlation_id And the record is retrievable via the audit API using filters for time range, initiator, target, scope, and result And only users with Audit:Read in the same tenant can view the record in UI/API And any attempt to alter or delete the record via UI/API is rejected with HTTP 403 and is itself logged
Time-Limited Shareable Snapshot Links
Given a permitted user creates a snapshot link from an active simulation session When the link is generated Then the snapshot reproduces the exact simulated state (permissions, UI visibility, and redactions) for reviewers And the link includes an explicit expiration timestamp and cannot be valid beyond the data retention window And once expired, accessing the link returns HTTP 410 Gone and no data is rendered And the snapshot view is read-only and prevents navigation outside the simulated scope
Tenant-Isolated Reviewer Access
Given a reviewer opens a valid snapshot link When the reviewer authenticates Then access is granted only if the reviewer belongs to the same tenant and has Snapshot:View permission And reviewer activity (open, refresh, expire) is logged with reviewer_user_id, snapshot_id, and utc_timestamp And attempts to use the link from a different tenant or without required permission are denied with HTTP 403 and logged And the reviewer cannot access live tenant data or other tenants from the snapshot view
Policy-Driven Redaction Across Channels
Given the tenant redaction policy flags specific fields as sensitive When simulation data is logged, exported, streamed to SIEM, or viewed via a snapshot Then sensitive fields are redacted per policy (masking or nulling) while preserving field names and types And redaction is enforced server-side and cannot be bypassed via client parameters And search and filtering operate on redacted values only and never reveal original sensitive values
CSV/JSON Export and SIEM Integration
Given an admin selects a date range and optional filters (initiator, target, scope, result, action_type) When the admin exports audit records Then the system produces downloadable CSV and JSON files whose records exactly match the API query results (count and field values subject to redaction) with UTC timestamps and documented headers/keys And the system logs an export event with requester, filters, format, record count, and utc_timestamp And when SIEM integration is configured, a test event succeeds and new audit events are delivered to the SIEM within 60 seconds with at-least-once delivery and redaction applied
Default 30-Day Retention and Purge
Given the platform default retention is 30 days for logs and snapshots When an audit record or snapshot reaches the end of its retention period Then it becomes inaccessible via UI, API, snapshot links, and SIEM streaming And it is purged from storage and cannot be recovered And snapshot links cannot be created or extended to outlive the remaining retention period And admins can view the scheduled purge date for each record or snapshot
Admin Usage and Impact Reports
Given an admin opens the Role Simulator usage report for a selected date range When the report is generated Then it displays counts of simulation sessions, permission queries, snapshot links created, opened, and expired, and unique initiators And it displays permission error indicators (e.g., count of simulated denies) and trend lines over time And the report can be exported to CSV and filtered by initiator, target role, and result And if support ticket metrics are provided (via integration or import), the report shows totals and percentage change versus baseline; otherwise, the section indicates not configured
Performance, Reliability, and Monitoring SLOs
"As an admin under time pressure, I want simulations and permission checks to load quickly and reliably so that I can make decisions without delay."
Description

Define and meet SLOs for the Role Simulator: simulator cold start <2s p95, route switch <500ms p95, permission query latency <250ms p95, and 99.9% monthly availability. Implement caching, incremental hydration, and circuit breakers with graceful degradation when policy or content services are unavailable. Provide observability dashboards (traces, metrics, logs), synthetic tests for key flows, and alerting tied to SLOs. Include automated load tests to validate scale across peak class scheduling windows.

Acceptance Criteria
Simulator Cold Start p95 < 2s
Given a first-time or cache-cold session during peak scheduling windows, When the user opens Role Simulator, Then time-to-interactive is <= 2000 ms at p95 over >= 10,000 sessions, And <= 1000 ms at p50, And measurements are captured via distributed traces and RUM, And no single backend dependency contributes > 800 ms at p95 to the end-to-end cold start path.
Route Switch p95 < 500ms
Given the simulator is already loaded, When a user switches between simulator routes or role contexts, Then the transition completes <= 500 ms at p95 and <= 250 ms at p50 over >= 5,000 transitions, And any loading indicator is visible for <= 300 ms at p95, And no client-side errors are logged at p95 during transitions.
Permission Query Latency p95 < 250ms
Given the simulator runs a 'Can X do Y on Z?' decision, When the policy decision is requested, Then end-to-end decision latency is <= 250 ms at p95 and <= 120 ms at p50 over >= 20,000 decisions during peak windows, And timeout for a single decision is set to 400 ms, And timeouts or errors are surfaced as non-blocking UI states without blocking other decisions.
99.9% Monthly Availability with Circuit Breakers and Graceful Degradation
Rule: Monthly availability SLI (successful simulator sessions per minute) >= 99.9% per calendar month; planned maintenance with >= 24h notice and <= 60 minutes/month is excluded. Rule: If a dependency (policy or content service) has >= 5% error rate for 1 minute or p95 latency >= 1000 ms for 2 minutes, open circuit within 30 seconds. Rule: While open, simulator degrades to read-only with a non-blocking banner; destructive actions and permission changes are disabled; 'Can X do Y on Z?' returns indeterminate and never grants access beyond least privilege. Rule: Auto half-open after 2 minutes of healthy checks; fully close after 5 consecutive healthy checks; no elevated permission state persists after recovery.
Caching and Incremental Hydration Effectiveness
Rule: Steady-state cache hit rate for permission and content metadata >= 80% during peak windows. Rule: Stale-while-revalidate TTL <= 5 minutes; stale reads must never over-permit; default to deny with 'awaiting policy' state when stale. Rule: Incremental hydration renders an interactive skeleton <= 200 ms at p95 and full hydration completes <= 1500 ms at p95 after cold start. Rule: Cache invalidation on role/permission change propagates globally <= 60 seconds at p95.
Observability Dashboards and SLO-Tied Alerting
Rule: Dashboards expose p50/p95/p99 for cold start, route switch, and permission decision latencies; monthly availability; error budget burn rate; per-region breakdown; with >= 30 days retention. Rule: Traces sample 100% of simulator sessions for 7 days post-release and >= 20% thereafter; logs and metrics are correlated via trace IDs. Rule: Page on-call when burn rate > 14x over 2h or > 6x over 24h, or any p95 exceeds its SLO for >= 5 consecutive minutes, or projected monthly availability risks falling below 99.9%; alerts deliver to the primary channel within 2 minutes.
Synthetic and Load Tests Validate Peak Scale
Rule: Synthetic checks run every 5 minutes per region covering cold start, route switch, permission decision, and degradation paths; two consecutive failures trigger paging; results feed SLO dashboards. Rule: Pre-release and weekly automated load tests ramp to 2x observed peak concurrent users and QPS from the last 30 days; pass if p95 latencies meet SLOs and overall error rate <= 0.1% with no sustained resource saturation (CPU < 75%, memory < 80%, DB connections < 80%) during 30 minutes steady state. Rule: Load tests output a capacity report showing >= 30% headroom per critical component.

Anomaly Watch

Behavior‑aware alerts that flag unusual access or activity—after‑hours exports, sudden permission escalations, or bulk roster edits. Auto‑lock risky sessions, require re‑auth, and send a digest to owners so issues are caught early without babysitting the system.

Requirements

Behavior Baselines & Anomaly Detection Engine
"As a studio owner, I want the system to detect unusual behavior in real time so that I’m alerted to risks before they impact bookings or payments."
Description

Build a real‑time engine that learns normal access and activity patterns per organization, role, and user (time-of-day, IP/geo, device fingerprint, action frequency, export volumes) and flags deviations such as after‑hours data exports, sudden permission escalations, bulk roster edits, geo‑velocity (“impossible travel”), and API burst usage. Generate a severity and confidence score per event, process streams with low latency, and avoid blocking core booking and payment flows. Respect privacy via data minimization and configurable retention. Surface anomalies to downstream modules (alerts, auto‑response, audit) via internal events.

Acceptance Criteria
Baseline Learning per Org/Role/User
Given 14 days of historic access and activity events for an org with ≥50 active users When the baseline training job runs Then per-user, per-role, and per-org distributions for time-of-day, IP/geo prefix, device fingerprint (hashed), action frequency, and export volumes are computed, versioned, and retrievable via the model API with ≥95% coverage of active users Given a new user with <50 events When inference is requested Then the engine falls back to role/org priors and marks confidence ≤60% with a cold_start flag Given ongoing event ingestion When 24 hours elapse Then baselines are incrementally updated without downtime and previous versions remain queryable for audit for ≥30 days (subject to retention)
Core Anomaly Types Detection
Given a user’s baseline active window is 08:00–18:00 local When a data export occurs at 02:00 Then an anomaly is emitted within 2 seconds with type=after_hours_export, severity ≥70, confidence ≥70, and reason_codes includes time_window_deviation Given a user’s typical permission level is below Owner When their role is escalated to Owner within 1 minute without an approved change ticket flag Then an anomaly is emitted with type=permission_escalation, severity ≥80, confidence ≥75 Given the baseline roster edit rate p95 is 10 edits per 10 minutes for the user When 50 roster edits occur within 10 minutes Then an anomaly is emitted with type=bulk_roster_edits, severity ≥70, confidence ≥70 Given last successful login geo=New York, US and a new login geo=London, UK occurs 30 minutes later When computed travel speed exceeds the configured threshold (e.g., >1000 km/h) Then an anomaly is emitted with type=impossible_travel, severity ≥90, confidence ≥80 Given an API key’s baseline request rate p95 is 30 req/min When 200 req/min are observed over a 1-minute window Then an anomaly is emitted with type=api_burst, severity ≥75, confidence ≥75
Severity and Confidence Scoring
Given any emitted anomaly When the event payload is produced Then it includes severity (0–100), confidence (0–100), risk_tier derived from thresholds (medium ≥50, high ≥75, critical ≥90), reason_codes[], baseline_snapshot_id, and primary dimension (user_id or role_id) Given a cold-start context (baseline support <50 events) When an anomaly is emitted Then confidence ≤60 and the cold_start flag=true, while severity is computed from rule risk and statistical deviation Given the same input event is reprocessed When severity and confidence are recomputed Then outputs are deterministic within ±2 points Given organization-level scoring thresholds are updated When new anomalies are produced Then risk_tier reflects the updated thresholds without service restart
Low-Latency and Non-Blocking Processing
Given live ingestion at 1000 events/second When anomalies occur Then model inference latency p95 ≤200 ms and end-to-end detection (ingest→publish) p95 ≤2 seconds Given sustained engine backpressure or outage When booking and payment flows execute Then those flows remain available with ≤3% added latency and 0 additional 5xx attributable to the anomaly engine; events are queued for ≤10 minutes with loss ≤0.1% only when queues are exhausted Given recovery from outage When the engine resumes Then queued events are processed in order and lateness metrics are emitted
Privacy, Data Minimization, and Retention Controls
Given IP addresses are processed When data is stored Then only IPv4 /24 or IPv6 /48 prefixes are persisted; full IPs are discarded Given device fingerprints are processed When data is stored Then a salted hash is persisted; raw fingerprint attributes are not stored Given default retention=30 days and allowable range 7–180 days per org When an admin sets retention to X days within the allowed range Then data older than X days (baselines, anomaly events, features) is purged within 24 hours Given a user deletion request (Right to Erasure) When the request is executed Then all baseline and anomaly records keyed to the user_id are purged within 24 hours and excluded from future inference
Internal Event Publication and Consumption
Given an anomaly is detected When the event is published Then it appears on the internal bus topic anomalies.v1 within 2 seconds and conforms to the schema {id, org_id, user_id, type, severity, confidence, risk_tier, occurred_at, detected_at, reason_codes[], baseline_snapshot_id, session_id?, geo?, device_hash, metadata} Given duplicate publishes or retries When consumers process events Then idempotency via event id prevents duplicate side effects downstream Given Alerts, Auto-Response, and Audit are subscribed When anomalies are published Then each consumer receives and acknowledges events within 2 seconds p95; failed deliveries are retried with exponential backoff up to 24 hours and are observable via metrics
Configurable Alert Policies & Thresholds
"As an admin, I want to customize what counts as risky for my studio so that alerts match our workflows and reduce false positives."
Description

Provide a policy editor for owners/admins to define which behaviors trigger alerts and at what thresholds (e.g., export size, edit counts, after‑hours windows, role changes), with severity levels, per‑role/space overrides, trusted device/IP allowlists, and quiet hours. Offer best‑practice templates out of the box and maintain versioned, auditable changes. Policies apply across web and mobile, and propagate instantly without downtime.

Acceptance Criteria
Create & Save Policy With Thresholds and Severity
Given I am an Owner or Admin on ClassTap with access to Anomaly Watch > Policy Editor When I create a new policy with: after-hours window 22:00–06:00 (studio timezone), export size threshold = 500 records, edit count threshold = 50 within 10 minutes, role change trigger enabled, and severity = High And I click Save Then the policy is validated (time window format valid; thresholds are positive integers; severity ∈ {Low, Medium, High, Critical}) And the policy is stored with a unique ID and version 1 And it appears in the policy list within 2 seconds with status Active And the create API responds 201 with the policy payload
Per-Role and Space Override Precedence
Given a Base Policy X with default settings And a Space override for "Downtown Studio" sets severity = High And a Role override for "Assistant Instructor" disables the export size trigger When an Assistant Instructor in "Downtown Studio" exports 600 records at 14:00 Then the Role override takes precedence over the Space override and Base Policy And no alert is generated for export size And all other enabled triggers from applicable policies remain in effect
Trusted Device/IP Allowlist Suppresses Alerts
Given IP 203.0.113.10 and device fingerprint DF-123 are added to the Trusted Allowlist And a policy triggers on after-hours exports with severity = High When a user from IP 203.0.113.10 using device DF-123 performs a 600-record export at 23:30 Then no alert is created for this event due to the allowlist And the event is logged with reason "suppressed: trusted source" And removing the IP or device from the allowlist causes the same action to trigger an alert on the next attempt
Quiet Hours Defer Non-Critical Notifications to Digest
Given Quiet Hours are configured as 21:00–07:00 local time, Monday–Sunday And a policy with severity = Low or Medium matches an event When the event occurs at 22:15 Then no real-time notification is sent And the event is queued into the next daily digest for Owners And the alert record is visible in the Alerts log with status = Deferred
Best-Practice Policy Templates Available and Customizable
Given I open the Policy Templates gallery When I view available templates Then at least 4 templates are displayed: "After-Hours Access", "Bulk Export Guard", "Permission Escalation Watch", "Bulk Edit Spike" And selecting a template shows a preview of triggers, thresholds, and severities And clicking "Use Template" creates a new draft policy pre-populated with the template values without modifying the original template And I can edit and save the draft as a new active policy
Versioned, Auditable Policy Changes With Diff and Revert
Given an existing policy P at version 3 When I change its export size threshold from 500 to 300 and Save Then version 4 is created immutably with timestamp, actor, and field-level diff And the audit log lists versions 1–4 with who, when, and what changed And choosing "Revert to v2" creates version 5 that restores v2 values And audit entries are read-only and exportable as CSV and JSON
Instant Cross-Platform Policy Propagation Without Downtime
Given a new or updated policy is saved at 10:00:00 UTC When a matching event occurs on web at 10:00:02 and on mobile at 10:00:03 Then the policy is enforced on both platforms within 5 seconds of save without service restart And no active user sessions are terminated due to propagation And no 5xx errors are logged due to policy propagation during a 10-minute soak test window
Automated Containment & Step‑up Authentication
"As an owner, I want suspicious sessions automatically constrained so that potential abuse is limited without me constantly monitoring."
Description

Enable configurable automatic responses for high‑severity anomalies: lock or pause the active session, require step‑up re‑authentication/MFA, temporarily revoke elevated permissions, rate‑limit or pause bulk actions, and auto‑expire API tokens. Support per‑policy playbooks with safe rollback and idempotency, ensure no data loss for in‑flight bookings, and log all actions for audit and post‑mortem analysis.

Acceptance Criteria
After‑Hours Export Triggers Auto‑Lock and Step‑Up MFA
Given a logged-in user session S initiates a data export outside configured business hours and policy P is set to lock session and require MFA for high-severity anomalies When the anomaly engine flags the event and containment is invoked Then session S is locked within 2 seconds, subsequent requests are blocked (API: 423 Locked with code CT-LOCK-001; UI: 403 with MFA prompt), and a step-up MFA challenge is presented And upon successful MFA within the configured window W, session S is unlocked and the user must explicitly re-initiate the export; the previous request is not auto-retried And if MFA fails or times out, session S remains locked until lock_duration expires or an authorized override occurs, both events recorded And all actions are logged with user_id, session_id, policy_id, anomaly_id, action_taken, correlation_id, and timestamps
Sudden Permission Escalation: Temporary Revocation with Safe Rollback
Given a user U elevates permissions to an admin role within session S and a high-severity escalation anomaly matches policy P When containment is triggered Then elevated permissions are revoked within 2 seconds; admin-only routes return 403 with error code CT-REV-001 and admin UI controls are hidden on next render And on clearance (owner approval or cooldown expiry), the exact prior role set is restored and captured in the audit trail (before/after roles, actor, timestamps) And ongoing non-admin operations continue without interruption or 5xx errors And revocation and restoration are idempotent; repeated attempts do not create duplicate grants or logs
Bulk Roster Edit Surge: Rate‑Limit/Pause Without Data Loss
Given a bulk roster edit job J exceeds thresholds defined in policy P (items/minute or concurrency) When containment is triggered Then the system pauses J or reduces its throughput to the configured limit L within 5 seconds; in-flight edits complete, queued edits remain pending And each roster item ends in a terminal state {completed, skipped, failed} with a reason code; no partial updates or duplicates are produced And resuming J after clearance continues from the last consistent checkpoint without reprocessing completed items; item counts reconcile And booking constraints prevent double-bookings; zero integrity violations are observed in validation queries
Risky Session: Auto‑Expire API Tokens and Enforce Re‑Auth
Given API client C uses token T tied to principal/session implicated in a high-severity anomaly under policy P When containment is triggered Then token T is invalidated immediately; subsequent calls return 401 with error code CT-TOKEN-EXPIRED and a WWW-Authenticate hint for step-up And after successful step-up MFA by the principal, a new token T2 is issued with a unique jti and TTL not exceeding the configured maximum; T remains unusable And token invalidation is idempotent; repeated invalidations generate no duplicate effects and a single audit record per idempotency key
Per‑Policy Playbooks: Deterministic, Idempotent Execution
Given multiple identical anomaly events for the same principal/session arrive within the deduplication window D When the playbook for policy P executes Then containment actions run at most once per deduplication key; subsequent duplicates are logged as deduplicated without reapplying actions And if multiple policies match, actions execute in deterministic order (severity then priority) and compose into a single final state per defined rules And playbook execution includes an idempotency key; retries are safe and produce no additional side effects
Containment During In‑Flight Booking: Atomic Completion or Rollback
Given a booking transaction T with payment authorization exists prior to a containment trigger affecting its session or permissions When the containment action would lock the session, revoke permissions, or pause related operations Then T either completes successfully or is rolled back atomically; no partial charges, orphaned reservations, or inventory inconsistencies occur And the user receives a clear outcome (success or rollback) with instructions to re-authenticate if required And monitoring shows no increase in double-bookings or payment mismatches during simulated containment tests
Auditability: Complete, Queryable, Tamper‑Evident Logs
Given any containment or step-up action A executed under policy P When A completes Then an audit record is persisted containing who/what/when/why/where (user_id, session_id, ip, device), policy_id, action, outcome, before/after state, correlation_id, and latency, and is queryable within 60 seconds And logs are exportable to CSV/JSON and stored in a tamper-evident manner (append-only or hash-chained) with verification metadata And logging has at-least-once delivery with idempotency keys; duplicate records are deduplicated and write failures are retried with alerting
Multi‑channel Alerts & Owner Digest
"As a studio owner, I want timely alerts and summaries so that I can quickly assess risk without babysitting the system."
Description

Deliver real‑time alerts via in‑app notifications, email, and optional SMS/push, with deduping and batching to prevent alert fatigue. Include key context (who, what, when, where, severity, confidence) and one‑click investigation links. Provide daily/weekly digests summarizing anomalies, trends, and recommended actions. Support per‑user notification preferences and fallback routing when primary channels fail.

Acceptance Criteria
Real-time Multi-channel Alert Delivery and Deduplication
Given an anomaly event with severity >= Medium and user U has in-app and email enabled When the system detects the event Then an in-app notification is created within 10 seconds and an email is sent within 60 seconds and both share the same Alert ID And if SMS/push is enabled for U Then an SMS/push notification is sent within 60 seconds with the same Alert ID Given duplicate events for the same actor and action occur within a 5-minute dedupe window When alerts are generated Then only one notification per channel per user is sent and duplicates are suppressed and logged with the Alert ID and suppression reason Given notifications for the same Alert ID are present across channels When the user views notifications in-app Then the UI displays a single grouped alert instance with channel indicators
Alert Context Completeness and One-click Investigation
Given any delivered alert Then the payload includes: who (actor name and user ID), what (action and rule name), when (ISO-8601 timestamp with timezone), where (IP, geo, device/OS), severity (Low/Medium/High/Critical), confidence (0–100%), and tenant/environment ID And the notification contains a one-click Investigate link pre-filtered by Alert ID, actor, and time window Given an authorized owner/admin clicks the Investigate link within 24 hours When the page loads Then the investigation view opens within 3 seconds scoped to the correct tenant and shows the alert details and related activity Given the link is expired or the recipient lacks permission When it is clicked Then access is denied without exposing sensitive details and the user is prompted to request access
Alert Batching to Prevent Fatigue
Given 3 or more alerts of the same rule type occur for the same tenant within a 10-minute batching window When notifications are composed Then a single batched notification per channel per user is sent including total count, highest severity, and top 3 examples with links Given batching occurs Then the email subject and in-app title include the count and highest severity Given per-user rate limiting is set to 10 notifications per hour per channel When alerts exceed the limit Then additional alerts are rolled into the next batch or summarized in the next digest and a suppression entry is recorded in the audit log Given a user opens a batched alert in-app Then they can expand to view individual alert IDs and navigate to each investigation
Per-user Notification Preferences and Overrides
Given a user updates notification preferences (channel toggles and severity threshold) When they save Then preferences persist and take effect within 1 minute and are reflected on subsequent alerts Given an alert is below the user’s severity threshold or the channel is disabled When the alert is generated Then no notification is sent via that channel Given org policy override_critical is ON When a Critical alert occurs Then at least one available channel not legally opted-out is used regardless of the user’s severity threshold and the override is logged Given a user subscribes to daily and/or weekly digests When digest time occurs Then the user receives only the selected digests via enabled channels
Fallback Routing on Channel Failure
Given email is the user’s highest-priority channel When an email send results in a permanent failure or 3 transient failures within 5 minutes Then the system attempts the next preferred enabled channel within 60 seconds and logs the fallback with error codes Given an SMS/push token is invalid When a send is attempted Then the channel is marked unhealthy for 30 minutes and the next channel is attempted Given all channels fail When delivery attempts complete Then an in-app notification is queued, an audit log entry records final failure with reasons, and no further attempts are made for that alert Given fallback succeeds on any channel Then the user receives only one successful notification per alert per channel
Daily Owner Digest Summary and Actions
Given the daily digest schedule is 7:00 AM in the org’s primary timezone When the digest job runs Then owners and subscribed users receive a digest covering the previous 24 hours via enabled channels And the digest includes: total anomalies, counts by rule, top 5 actors, severity distribution, percent change vs prior day, unresolved items list, and 3 recommended actions with links to investigate or configure Given zero anomalies and Send when zero is OFF When the digest job runs Then no daily digest is sent Given any digest link is clicked by an authorized user When the page loads Then the corresponding filtered view opens within 3 seconds
Weekly Owner Digest Trends and Recommendations
Given the weekly digest schedule is Sunday at 8:00 AM in the org’s primary timezone When the digest job runs Then owners and subscribed users receive a digest covering the previous 7 days via enabled channels And the digest includes: anomalies-by-day trend, top new anomaly types, MTTA and MTTC, top risky locations/devices, and prioritized recommended actions Given a recipient has opted out of weekly digests When the digest job runs Then the recipient does not receive the weekly digest Given the digest content exceeds 5 MB When composing the email Then the email includes a link to view the full report in-app instead of embedding heavy content
Alert Center & Resolution Workflow
"As a manager, I want a centralized place to review and resolve alerts so that we can track actions and close issues efficiently."
Description

Create a workspace to review, filter, and search anomalies; inspect enriched context (session details, IP/geo, change diffs, related bookings/payments); and take actions (mark resolved, confirm incident, require re‑auth, whitelist, assign to teammate). Track status lifecycle (new, investigating, resolved), add notes, and export incident reports. Provide role‑based access controls so only authorized users can view sensitive details.

Acceptance Criteria
Alert Center Filtering and Search
Given I am an authorized user on the Alert Center, When I apply filters by type, severity, status, and date range, Then the list updates within 1 second and shows only matching alerts with the active filters visible. Given I enter a query containing user email, IP, session ID, or alert ID, When I run the search, Then the results include all alerts with matching fields and highlight matched terms. Given the results exceed 50 items, When I paginate or change page size, Then item counts remain accurate and my page/size preferences persist in the URL. Given I sort by time or severity, When I toggle the sort, Then ascending/descending order is applied and reflected in the URL and retained on refresh. Given no items match, When filters or search return zero results, Then an empty state appears with a Clear Filters action that resets the view to default.
Enriched Context Panel
Given I open an alert, When I view its context panel, Then I see session ID, actor (user and role), IP with geo (city/country), device/OS/browser, and event timestamp rendered in my locale timezone. Given the alert is a permission change, When I view change details, Then before/after permissions are shown with changed fields highlighted. Given the alert is tied to bookings or payments, When I open Related Items, Then linked booking/payment IDs are listed with key attributes (status, amount, timestamp) and open in a new tab. Given sensitive fields are displayed, When my role lacks clearance, Then IPs are masked to /24, emails are partially obfuscated, and tokens/authorization headers are fully redacted. Given the context panel loads, When data retrieval succeeds, Then content renders within 800 ms; When retrieval fails, Then a non-blocking error is shown with a Retry control.
Action Execution and Audit Logging
Given an open alert, When I click Confirm Incident and confirm, Then the alert status becomes Investigating and an audit entry is recorded with actor, timestamp, action, and previous/new status. Given an open alert, When I click Require Re-Auth for the actor and confirm, Then the actor's active session is invalidated within 5 seconds and the alert shows an Action Applied badge; an audit entry captures the target and outcome. Given a repeat benign pattern, When I apply a whitelist rule (scope: IP/session/action type, duration selected), Then subsequent matching alerts are suppressed for the selected duration and the rule appears in Allowlist Management with the creator and expiry. Given an alert, When I assign it to a teammate, Then the assignee field updates immediately and is visible in the list and detail views with an audit record of the assignment. Given any action is performed, When I open the audit trail, Then all actions are listed in chronological order, immutable, and exportable with actor, timestamp, action, parameters, and outcomes.
Status Lifecycle and Notes
Given an alert in New, When I change its status to Investigating, Then the status updates, started_at is recorded, and the transition appears in the history. Given an alert, When I attempt to Resolve it, Then a note of at least 10 characters is required; upon submission the status becomes Resolved with resolved_at recorded and the note appended. Given a Resolved alert, When I Reopen it, Then the status returns to Investigating and the history reflects the transition with actor and timestamp. Given a note is added to any alert, When I save the note, Then it is timestamped, attributed to the author, non-editable, and searchable; deletions are disallowed. Given the alerts list, When it renders, Then each alert shows a status badge and the header shows accurate counts by status (New, Investigating, Resolved).
Export Incident Report
Given a filtered alert list, When I export to CSV, Then a file is downloaded within 5 seconds for up to 5,000 rows and includes one row per alert plus a header summarizing the active filters and export timestamp (UTC). Given an alert detail view, When I export to PDF, Then the report contains alert metadata, enriched context, change diffs, related items, status history, notes, and audit trail, with sensitive fields redacted per RBAC. Given I lack export permissions, When I attempt any export, Then the request is denied with a 403 error and no file is produced. Given a large result set over 5,000 rows, When I request export, Then I am informed an asynchronous export will be prepared and a downloadable link is provided when ready.
RBAC for Sensitive Details
Given my role is Owner or Admin, When I access the Alert Center, Then I can view all alerts and unmasked sensitive details as permitted by policy. Given my role is Instructor/Staff, When I attempt to access the Alert Center, Then I receive a 403 error and no alert metadata or PII is leaked in the response. Given my role is not Owner/Admin, When I view an alert allowed to me, Then IPs are masked to /24, emails show only first/last character with asterisks, and payment details show last 4 digits only. Given I attempt to bypass UI controls, When I call the alerts API without sufficient scope, Then the server enforces RBAC and returns 403 regardless of guessed IDs. Given any access to alert details, When it occurs, Then an access log entry is recorded with user, time, resource, and outcome for compliance review.
Forensic Audit Logs & Integrations
"As a compliance‑minded owner, I want detailed, exportable records of anomalies and responses so that we can audit incidents and meet policy obligations."
Description

Capture immutable, time‑sequenced logs for all anomalies and responses, including actor, target objects, before/after diffs for permissions and rosters, IP/device fingerprints, geo, and policy version used. Support configurable retention, CSV export, webhooks, and SIEM integrations. Expose a secure API for querying incidents. Apply data minimization and access controls to meet GDPR/CCPA requirements.

Acceptance Criteria
Immutable Anomaly and Response Logging
Given an anomaly is detected or a response action is taken (auto-lock, re-auth required, permission change, roster bulk edit) When the event is persisted Then a new log record is appended with a monotonically increasing id, ISO 8601 UTC timestamp (ms), actor id and role, session id, target object type and id, policy version id evaluated, event type and outcome, before/after diffs (JSON Patch) for permissions and rosters, source ip (masked per role), device fingerprint (salted hash), and geo (country, region, city) Given the log is written When integrity is verified Then the record includes a content hash and previous-hash forming a hash chain and the store enforces append-only (WORM); any correction results in a new record that references the prior record Given multiple events occur within the same millisecond When queried Then ordering is deterministic by (timestamp, id) and no gaps exist in id sequence for a tenant
Configurable Retention and Legal Hold
Given workspace retention is set to R days (30 <= R <= 3650) When a log record exceeds R and is not under legal hold Then it is purged within 24 hours and a purge audit event is recorded with counts and timestamp Given legal hold is enabled for an incident or time range When retention would purge held records Then records are preserved until the hold is cleared and all changes to holds are audited with actor and reason Given retention configuration is updated When the change is saved Then the effective date and previous value are captured in audit logs and the next purge job is idempotent and reports successes/failures
CSV Export with Role-Based Redaction
Given an Owner or Auditor requests a CSV export with filters (time range <= 31 days, event types, actor, object type) and selected columns When the export is processed Then it completes within 10 minutes for up to 5,000,000 rows, split into parts <= 250 MB each with SHA-256 checksums and a signed URL expiring in 24 hours Given role-based redaction rules When a non-Owner downloads the export Then sensitive fields (full IP, raw device fingerprint, email) are masked/truncated; before/after diffs are included as flattened JSON with redacted values Given the export is generated When validated Then row count matches the query, headers include schema_version and timezone=UTC, and values are RFC4180-compliant and properly escaped
Incident Webhook Delivery and Reliability
Given a webhook with secret S and event filters is configured When a matching incident occurs Then the system POSTs a JSON payload within 5 seconds including id, timestamp, severity, actor, target, policy_version_id, diffs, and an X-Signature=HMAC-SHA256(payload,S) over TLS 1.2+ Given delivery fails (non-2xx or timeout) When retries are attempted Then the system retries up to 6 times with exponential backoff (1s, 2s, 4s, 8s, 16s, 32s), includes an Idempotency-Key header, and stops on first 2xx Given a webhook secret is rotated When new deliveries occur Then both old and new secrets are honored for up to 1 hour; rotation events and all deliveries are logged with outcomes
SIEM Streaming and Health Management
Given a SIEM destination (Syslog over TLS or HTTPS) is configured When incidents are generated Then events are streamed in JSON mapped to ECS or in CEF as configured, including tenant_id, event.category, event.action, actor, target, policy.version, and diffs; batches respect max 1 MB or 5,000 events, whichever comes first Given destination backpressure or errors When buffers fill Then the system queues up to 1,000,000 events per tenant, drops oldest beyond that with counters, surfaces an alert to Owners, and exposes delivery metrics and last_success timestamp Given connectivity issues persist for 5 minutes When health checks fail Then integration status transitions to Degraded and back to Healthy on recovery; all status changes are audited
Secure Query API with Access Controls
Given a caller requests GET /v1/audit/incidents with OAuth2 scope audit:read When the caller has Owner or Auditor role for the workspace Then the request succeeds with 200; otherwise return 403 (authenticated without rights) or 401 (unauthenticated) Given filters (time range <= 93 days, actor, event_type, object_type, severity) When the request is executed Then results are filtered accordingly, paginated with a stable cursor, sorted by timestamp desc then id desc, and the response includes next_cursor and count Given role-based minimization and tenant isolation When a non-Owner queries Then sensitive fields are masked consistent with export rules and only records from the caller’s workspace are returned; rate limits enforce 120 requests/min per token with 429 on exceed; p95 latency <= 800 ms for up to 10,000 items
Data Minimization and Subject Rights Compliance
Given default privacy settings When events are logged Then IP addresses are stored with the last octet zeroed by default and full IP shown only to Owners; device fingerprints are stored as salted hashes; geo is limited to city-level unless elevated privileges are granted Given a Data Subject Access Request (DSAR) for an email or user id When executed by an Owner Then the system exports subject-related incidents within 30 days with third-party PII masked, and logs the DSAR execution with actor, scope, and timestamp Given a deletion (erasure) request with no legal hold When executed Then subject identifiers in logs are pseudonymized (tokenized) while preserving referential integrity; a record of the request and outcome is logged; all access to audit logs is itself audited
Booking & Payments Anomaly Coverage
"As a studio owner, I want the system to flag and pause suspicious booking or payment activity so that revenue and schedules aren’t disrupted by misuse or errors."
Description

Extend detection with domain‑specific rules for ClassTap: flag mass discount creation, rapid refund attempts, unusual invoice export volumes, spikes in waitlist‑to‑payment conversions, and attempts to circumvent double‑booking protections. Tie into scheduling and billing services to pause or require re‑auth on risky transactions, with clear user messaging and safe retry paths.

Acceptance Criteria
Mass Discount Creation Anomaly
- Given tenant thresholds maxDiscountCodesPer10Min=5 and maxClassesImpactedPer10Min=3, When a single actor creates 6 or more discount codes across 4 or more distinct classes within 10 minutes, Then the system flags ANOM_DISCOUNT_BURST, blocks further discount creation for that actor for 15 minutes, and requires re-authentication to continue. - Given an anomaly is flagged, When the actor attempts to create another discount code during the lockout, Then the UI shows an inline banner with errorCode=ANOM_DISCOUNT_BURST explaining the pause and providing a re-auth CTA within 2 seconds. - Given the actor completes re-auth successfully within 5 minutes, When they click Retry once, Then the attempted action is re-submitted exactly once and succeeds if within policy; otherwise a clear error is shown and no duplicate codes are created. - Given ANOM_DISCOUNT_BURST, Then an owner digest entry is queued including actorId, countCreated, classesImpacted, actionTaken=LOCK+REAUTH, and delivered within 5 minutes. - Then an audit log record is written with correlationId, thresholdsUsed, timestamp, and outcome, and is queryable by tenant admins.
Rapid Refund Attempts Anomaly
- Given tenant thresholds maxRefundsPer2Min=3 or maxRefundAmountPer2Min=500 USD, When a single actor initiates 3 or more refunds OR accumulates 500 USD or more in refunds within 2 minutes, Then the system flags ANOM_REFUND_BURST, pauses further refund processing for that actor, and requires re-auth before any additional refunds. - When paused, Then any in-flight refunds not yet settled are marked as Held with holdReason=ANOM_REFUND_BURST and are not sent to the payment processor. - Given the actor re-authenticates successfully, When they retry a Held refund, Then one refund is released and processed; bulk release is not permitted without owner approval. - Then owners receive a digest entry summarizing refundCount, totalAmount, classesAffected, and actions taken within 5 minutes. - Then audit logs include per-refund outcomes, hold/release timestamps, and the payment processor transaction IDs (if any).
Unusual Invoice Export Volume
- Given tenant thresholds maxExportsPer5Min=3 and afterHoursWindow=20:00–08:00 local, When an actor triggers 4 or more invoice exports in 5 minutes OR any export occurs after hours, Then the system flags ANOM_EXPORT_SPIKE and requires re-auth before allowing additional exports in that session. - Then the current export request completes if already started but subsequent export actions are blocked with a message including errorCode=ANOM_EXPORT_SPIKE and a re-auth CTA. - Given successful re-auth, When the actor retries once, Then a single export is allowed; further bulk exports require owner approval or a 30-minute cooldown. - Then a digest entry is sent to owners with actorId, exportCount5Min, timeWindow, and actionTaken=REAUTH within 5 minutes. - Then an immutable audit record captures fileIds/hash, export filters, destination, timestamp, and IP/device fingerprint.
Spike in Waitlist-to-Payment Conversions
- Given tenant thresholds minConversions10Min=10 and spikeMultiplier=3x over the 7-day rolling average for the same class and timeslot, When waitlist-to-payment conversions reach 10 or more within 10 minutes AND exceed 3x the baseline, Then the system flags ANOM_WL_PAY_SPIKE and automatically pauses further auto-conversions for that class for 30 minutes. - When paused, Then users attempting to convert from waitlist see a message explaining the temporary pause with errorCode=ANOM_WL_PAY_SPIKE and are offered a re-auth + manual checkout path. - Given successful re-auth, When a user proceeds via manual checkout, Then a single conversion is allowed and logged as MANUAL_OVERRIDE with correlationId. - Then owners receive a digest summarizing classId, conversions10Min, baseline, pauseDuration, and overrides within 5 minutes. - Then audit logs include queue snapshot, payments authorized/settled, and any overrides with actorId and reason.
Double-Booking Protection Circumvention Attempts
- Given double-booking protection is enabled and class capacity > 0, When concurrent requests from the same actor or API key attempt to book overlapping time slots for the same attendee OR roster edits push attendance above capacity, Then the system rejects the requests, flags ANOM_DOUBLE_BOOK_BYPASS, locks the session for scheduling actions for 10 minutes, and requires re-auth to continue. - When the anomaly is triggered, Then no over-capacity or overlapping bookings are created; capacity and rosters remain consistent across services within eventual consistency of ≤2 seconds. - Given re-authentication succeeds, When the actor retries a single allowed booking within policy, Then it succeeds; attempts to exceed capacity again are blocked with the same error code. - Then owners receive a digest with details: actorId, classIds, attemptedOverage, collisionType (overlap|capacity), and actionTaken=LOCK+REAUTH within 5 minutes. - Then audit logs contain requestIds, concurrency markers, final roster counts, and rejection reasons.
Risk Response Orchestration and Safe Retry
- Given any anomaly code in {ANOM_DISCOUNT_BURST, ANOM_REFUND_BURST, ANOM_EXPORT_SPIKE, ANOM_WL_PAY_SPIKE, ANOM_DOUBLE_BOOK_BYPASS}, When triggered, Then orchestrated actions are invoked: scheduling/billing operations are paused per policy, re-auth is required, user-facing messaging includes errorCode and next steps, and a single-click Retry is presented post re-auth. - Then re-auth uses the tenant’s configured method (passwordless/email OTP/SAML) and must complete within 5 minutes; otherwise the action remains paused and no side effects occur. - When Retry is clicked after successful re-auth, Then exactly one idempotent retry is executed using the original parameters, with duplicate-prevention via idempotencyKey and correlationId. - Then all anomaly events publish to the security_event stream with fields {tenantId, actorId, anomalyCode, thresholds, actionTaken, correlationId} within 2 seconds, and owner digests are sent within 5 minutes. - Then accessibility and localization are respected for banners and dialogs (WCAG AA, locale from tenant settings).

RollingQR

Continuously refreshes each attendee’s QR code every few seconds with server‑signed, short‑lived tokens tied to their device and event. Blocks screenshots and forwards while keeping lines moving—codes update automatically in the browser or mobile wallet, no app relaunch needed.

Requirements

Server-Signed Rotating Tokens
"As a door staffer, I want rotating, server-signed codes that expire quickly and are tied to a device so that screenshots and forwarded codes cannot be used to enter."
Description

Implements a backend service that issues short-lived, event- and device-scoped tokens embedded in QR payloads and rotated every few seconds. Tokens carry claims for attendee, booking, event, device hash, iat/exp, nonce, and permitted uses, and are signed with asymmetric keys (KMS-backed, with rotation). The service enforces replay protection, clock-skew tolerance, revocation lists, and instant invalidation on refund/cancel. This ensures screenshots and forwarded codes fail server validation while keeping issuance aligned with booking and payment status.

Acceptance Criteria
Token Structure and Signature Validation
Given a request to issue a QR token for a valid, paid booking When the service generates the token Then the JWT/JWS payload includes attendee_id, booking_id, event_id, device_hash, iat, exp, nonce, and permitted_uses claims And the token is signed with an asymmetric KMS-backed key using an allowed algorithm (RS256 or ES256) And the token size does not exceed 1024 bytes And any missing/invalid claim or unsupported algorithm causes issuance or validation to fail with a 4xx error Given a presented token When the validator checks the signature against the current JWKS Then the signature verifies against an active key And tokens signed with unknown/expired keys are rejected with a 401 error
Rotation Interval and TTL Enforcement
Given the rotation interval is set to 8s (configurable 5–15s) and token TTL is 10s When a client requests tokens continuously for 30s Then a new token is issued at least every 8±1s And each token's exp is no more than 10s after iat And a fresh token is available at least 1s before the previous token's exp And any token presented after exp is rejected with 401 within 200ms Given intermittent network delay up to 500ms When rotation occurs Then no more than one rotation is skipped, and the next issuance resumes within the next interval
Device Binding and Anti-Forward/Screenshot
Given a token issued for device_hash D1 When the token is presented from a device producing device_hash D1 Then validation succeeds with 200 Given the same token is presented from a different device producing device_hash D2 When the validator compares device_hash Then validation is rejected with 401 and reason "device_mismatch" Given a static screenshot or forwarded QR image of a valid token When it is scanned from a device not matching the original device_hash Then validation fails with 401 And no new tokens are issued to the mismatched device
Replay Protection with Nonce
Given a freshly issued token with nonce N When it is presented for validation the first time within its TTL Then validation succeeds and nonce N is marked as consumed for that booking/device Given the same token with nonce N is presented again (replay) within the TTL or within a replay window of 30s after exp When validation occurs Then validation is rejected with 409 and reason "replay_detected" Given two concurrent validations of the same token arrive within 100ms When processed by the service Then at most one succeeds and the other is rejected as replay
Revocation and Instant Invalidation on Cancel/Refund
Given an attendee has an active booking with rotating tokens being issued When the booking status changes to canceled or refunded Then token issuance for that booking stops within 1s And any currently valid token becomes invalid within 2s And subsequent validations of prior tokens return 403 with reason "revoked" Given the booking is reinstated When issuance is resumed Then new tokens are issued with new nonces and prior tokens remain invalid
Key Management and Rotation (KMS-Backed)
Given the signing key is rotated in KMS When new tokens are issued post-rotation Then they are signed with the new active key And the validator publishes an updated JWKS within 60s of rotation Given tokens issued before rotation When validated during a 5-minute overlap window Then they verify against the previous key and succeed And after the overlap window expires, such tokens are rejected with 401 Given a key is explicitly disabled or revoked in KMS When issuance is attempted with that key Then issuance fails and the system selects an active key or returns a 5xx with alerting
Clock Skew Tolerance
Given client device time is ahead of server by up to 2s When a token is issued and presented immediately Then validation succeeds using a ±2s iat/exp skew window Given client device time is behind server by up to 2s When a token nearing exp is presented Then validation succeeds if presented within exp+2s Given client-server clock skew exceeds 2s When the token is presented Then validation fails with 401 and reason "clock_skew_exceeded" And the event is logged with the measured skew
Auto-Refresh QR Client
"As an attendee, I want my QR code to refresh automatically in my browser or wallet without reopening the app so that I can move through check-in quickly."
Description

Delivers a web/PWA and wallet-compatible client that renders the attendee’s QR and refreshes it automatically without relaunch. Supports WebSocket/SSE push or prefetch of the next signed tokens with local timers, visual countdown indicators, and seamless background/foreground handling. Includes network resilience (prefetch buffer, exponential backoff) and mobile wallet pass updates via platform push, ensuring the on-screen code stays current in browsers and mobile wallets with minimal user action.

Acceptance Criteria
Web/PWA Auto-Refresh via Push Stream
Given an authenticated session with an active WebSocket or SSE connection When the server sends a rotate event containing the next signed token with an exp timestamp Then the client replaces the displayed QR within 500 ms and never shows a token past its exp timestamp Given the push stream remains connected When tokens are rotated for 5 consecutive cycles Then each rotation completes without a blank frame or visual flicker and the QR payload changes each cycle
Prefetch and Local Timer Fallback
Given push connectivity is unavailable and the client can reach HTTPS When the client prefetches the next token(s) Then it maintains a buffer of at least 1 upcoming token and schedules refresh based on server-supplied exp timestamps Given the current token is within 3 seconds of expiry and a prefetched token is available When no push event has arrived Then the client swaps to the next token no later than 1 second before expiry and attempts to prefetch the subsequent token Given connectivity is lost and the prefetch buffer is empty When the current token expires Then the app hides the QR within 2 seconds and shows a reconnect indicator (no expired QR remains visible)
Visual Countdown and Status Indicators
Given a displayed token with a server-supplied exp timestamp When the countdown is shown Then remaining time is displayed with accuracy within ±200 ms and switches to a warning style in the last 5 seconds Given assistive technologies are enabled When the countdown updates Then screen readers announce remaining time no more than once per second and color contrast meets WCAG AA for text and icons Given continuous rotations When the QR and countdown update Then there is no perceptible flicker and no blank state between rotations
Background and Foreground Handling
Given the app moves to background for less than 3 minutes with network available When it returns to foreground Then the displayed token is valid within 500 ms without any user action Given the app returns after the previous token has expired When visibility changes to foreground Then the client requests a fresh token immediately and displays it within 1 second Given iOS/Android PWA background suspension constraints When the app is resumed Then token rotation resumes and any required fetch occurs within 1 second of resume
Network Resilience and Backoff
Given the push stream disconnects unexpectedly When reconnecting Then the client applies exponential backoff delays of 1s, 2s, 4s, 8s, capped at 60s with ±20% jitter, and resets the backoff after a successful connection Given intermittent connectivity affects prefetch When prefetch attempts fail Then at least one retry occurs before expiry and the client aims to keep 1 upcoming token buffered; no expired token remains visible beyond 2 seconds Given a token fetch returns 401 or indicates invalid device binding When handling the response Then the client clears local buffers, stops rotation, displays a re-auth prompt, and does not display an expired or invalid QR
Mobile Wallet Pass Auto-Update
Given a wallet pass is installed and the device receives a platform push update containing a new token When the pass is opened within 10 seconds of the push Then the QR updates to the latest token within 2 seconds of opening without manual refresh Given the wallet pass is open when a push update arrives When the platform delivers the update Then the QR replaces within 2 seconds and never shows a token past its exp timestamp Given the device is offline When the wallet pass is opened Then the pass shows the last valid (unexpired) QR; if expired, it displays a connectivity required state and does not show an expired QR
Device Binding & Anti-Forwarding Controls
"As an organizer, I want codes bound to each attendee’s device with safeguards against copying so that only the rightful attendee can use their pass."
Description

Binds tokens to a device fingerprint (e.g., wallet pass ID, platform, UA hints) and enforces device match at scan time. Adds mitigations including one-time-use semantics, short TTLs, attendee watermark (initials/time) to discourage sharing, and optional brightness lock/fullscreen prompts. Provides a secure rebind flow with step-up verification when users switch devices, audit logging for mismatches, and admin policies for how strictly to enforce binding by event type.

Acceptance Criteria
Scan-time device binding enforcement
Given an attendee has a server-signed RollingQR token containing deviceHash H1 and eventId E, And the attendee’s enrollment for E is bound to deviceHash H1, When the token is scanned at check-in, Then the server returns 200 with outcome CHECKED_IN=true within 800 ms and records attendance exactly once. Given a token containing deviceHash H2 that does not match the stored binding H1 for the attendee-event, And the event policy is Strict, When the token is scanned, Then the server returns 403 with reasonCode=DEVICE_MISMATCH, no attendance record is created, an audit event is stored with {eventId, attendeeId, tokenId, presentedDeviceHash:H2, boundDeviceHash:H1, timestamp, scannerId, policy:"Strict"}, and the check-in UI displays "Device mismatch" within 500 ms. Given the event policy is Warn, When a mismatch is detected, Then the server returns 200 with outcome CHECKED_IN=true, and an audit event is stored with severity=warn and reasonCode=DEVICE_MISMATCH.
One-time-use token invalidation
Given a valid unscanned token T for attendee A and event E, When T is scanned successfully, Then attendance for A at E is marked, T is marked used with usedAt timestamp, and the response includes firstUsedAt. Given T has been marked used, When T or any replay of its payload is scanned again, Then the server returns 409 with reasonCode=ALREADY_USED, includes firstUsedAt, no new attendance is created, and an audit event with type DUPLICATE_SCAN is recorded. Given two scans of the same attendee occur within 2 seconds due to double-scan, When processed concurrently, Then exactly one attendance record exists (idempotent) and the duplicate request returns ALREADY_USED.
Short TTL and rolling refresh
Given an event E with tokenTTLSeconds=15 (default), When a token issued at iat is scanned after iat+15s, Then the server returns 410 with reasonCode=EXPIRED_TOKEN and no attendance is recorded. Given the QR view is visible and network is available, When time to expiry is <=5s, Then a new token is fetched and rendered so that a valid token is always available prior to expiry, with refresh cadence at least every 5s. Given the client loses network connectivity, When a token reaches expiry, Then the UI freezes the QR, overlays "Offline — QR paused" within 1s, and no further token refresh attempts succeed until connectivity is restored; any scan of a frozen token older than TTL returns EXPIRED_TOKEN. Given admin updates tokenTTLSeconds to a value between 10 and 60 inclusive, When tokens are subsequently issued, Then their exp reflects the new TTL within 60 seconds of configuration change.
Attendee watermark rendering
Given the QR is displayed in browser or wallet, When the token is rendered, Then a semi-transparent watermark shows attendee initials (from profile) and a minute-level timestamp (HH:mm) and updates with each token refresh. Given a screenshot is taken of the QR view, When the image is reviewed, Then the watermark is visible in the screenshot and cannot be disabled via UI controls. Given the watermark overlays the QR, When scanning under normal lighting, Then scan success rate is not reduced by more than 2 percentage points compared to the same QR without watermark (A/B measured across at least 500 scans).
Brightness and fullscreen prompts (optional)
Given the QR view is opened on a mobile device with supported brightness control and current brightness <80%, When the user accepts the prompt to maximize brightness, Then device brightness is set to 100% until the view is closed or the user manually lowers it. Given the platform does not allow programmatic brightness changes or the user declines, When the QR view is opened, Then a non-blocking banner instructs the user to increase brightness manually, and token refresh/scan remain unaffected. Given the QR view is opened, When platform supports fullscreen, Then the view requests fullscreen and hides system UI; if not supported, a prompt offers manual fullscreen entry. In all cases, token refresh cadence and scan performance are unaffected (no more than 100 ms additional render latency).
Secure device rebind with step-up verification
Given a user attempts to access their pass on a new device where computed deviceHash Dnew != stored deviceHash Dold and the event policy allows rebind, When the user completes step-up verification (admin-configured: email OTP, SMS OTP, or TOTP) within 5 attempts/hour, Then Dold is revoked, Dnew is bound, all prior tokens are invalidated within 2 seconds, and a success confirmation is shown. Given the user fails verification or exceeds the attempt limit, When rebind is attempted, Then rebind is denied with reasonCode=VERIFICATION_FAILED, a 15-minute cooldown is enforced, and an audit event REBIND_BLOCKED is stored. Given rebind succeeds, When subsequent scans occur, Then tokens issued for Dnew validate and tokens tied to Dold return 403 with reasonCode=DEVICE_MISMATCH.
Admin policies and audit logging
Given an admin sets device binding enforcement to Off for event E, When scans occur, Then device mismatch checks are skipped and no mismatch audits are recorded. Given an admin sets enforcement to Warn for event E, When a mismatch occurs, Then the scan passes, an audit log entry is recorded with severity=warn and reasonCode=DEVICE_MISMATCH, and the mismatch counter increments for E. Given an admin sets enforcement to Strict for event E, When a mismatch occurs, Then the scan fails with 403 DEVICE_MISMATCH, an audit log entry is recorded with severity=error, and (if notifications enabled) an alert is sent to the on-site lead within 30 seconds. Given audit logs are generated, When the admin searches by eventId, attendee email, or date range, Then matching records are returned within 2 seconds and can be exported to CSV containing at least {timestamp, eventId, attendeeId, tokenId, policy, outcome, reasonCode, scannerId} with 90-day retention.
Scanner Validation & Revocation
"As a door staffer, I want scans to validate instantly and reject reused or revoked codes so that lines move fast and fraud is prevented."
Description

Provides a low-latency validation path for the check-in scanner that verifies signatures, claims, revocation status, and reuse within <200ms. Supports offline venue mode using public-key verification, nonce/replay tracking, and short-term allowlists with later sync. Enforces single-admit or limited-use policy, rate-limits repeated failures, and propagates instantaneous revocations on refund/cancel. Includes observability for scan outcomes and latency.

Acceptance Criteria
Online Scan Validation ≤200ms
Given a connected scanner and a valid RollingQR token for the event And the token is not expired and not previously admitted beyond policy When the token is scanned Then the system verifies signature, claims (event, attendee, device binding, jti, iat/exp), revocation status, and reuse And returns an Accept/Reject decision with a reason code in ≤200 ms for at least 95% of scans over a rolling 5-minute window And the decision includes the resolved attendee reference for successful scans
Offline Venue Mode: Public-Key + Replay Protection
Given the scanner is offline with current event public keys and an allowlist TTL configured to 15 minutes And a RollingQR token with a valid signature, within TTL, and an unseen nonce/jti is presented When the token is scanned Then the scanner accepts the check-in and records the nonce/jti locally to prevent replay And upon reconnect, the admit is synced to the server within 60 seconds and deduplicated via nonce/jti When the same nonce/jti is presented again while offline Then the scanner rejects it within ≤200 ms with reason "ReplayDetected" And tokens outside TTL or with invalid signatures are rejected with explicit reason codes
Policy Enforcement: Single-Admit and Limited-Use
Given an event policy of Single-Admit or Limited-Use (L uses within window W) is configured When a token is scanned the first time within window W Then the scanner accepts and atomically records 1 use for the attendee When subsequent scans occur for the same attendee within window W Then the scanner accepts only if remaining uses > 0 and atomically decrements; otherwise rejects with reason "ReuseExceeded" And concurrent scans across multiple scanners result in at most one acceptance per remaining use
Instant Revocation on Refund/Cancel
Given an organizer issues a refund or cancel for an attendee prior to check-in When any connected scanner scans that attendee’s RollingQR token Then the decision is Reject with reason "Revoked" within ≤2 seconds of the refund/cancel action And all scanners receive the revocation via push/broadcast; offline scanners apply it on next sync and reject thereafter And if a previously admitted attendee is scanned after revocation, the result is Reject with reason "RevokedAfterAdmit" and the event is logged for audit
Rate-Limiting Repeated Failures
Given a scanner or source submits invalid tokens repeatedly When more than 5 failed scans from the same scanner_id or subject hash occur within 30 seconds Then subsequent scans from that source are rate-limited for 60 seconds and return reason "RateLimited" locally within ≤50 ms And valid scans from other sources are unaffected And rate-limit counters decay after 5 minutes without additional failures
Token Freshness, Signature, and Claim Validation
Given a RollingQR token is presented When validating the token Then the signature verifies against a trusted issuer key and supported algorithm And required claims (iss, event_id, attendee_id, device_binding, jti, iat, exp) are present and match the event context And token age (now − iat) ≤ 15 seconds and exp > now with up to ±60 seconds clock skew tolerance And any missing/invalid claim, expired token, or unsupported algorithm yields Reject with a specific reason code
Observability: Outcomes and Latency Metrics
Given scan decisions are produced When a scan is completed Then a structured log is emitted within 1 second containing scan_id, scanner_id, event_id, attendee_hash, token_jti, decision, reason_code, latency_ms, online/offline, policy_applied, timestamp And metrics report per-outcome and per-reason counters and latency histograms with p50/p95/p99 And a dashboard displays p95 latency ≤ 200 ms and failure rates by reason over the last 15 minutes And each scan is traceable via scan_id across services for debugging
Lifecycle & Payment Sync
"As an organizer, I want QR issuance and revocation to track booking, payment, and waitlist changes so that access always reflects current eligibility."
Description

Integrates RollingQR issuance and revocation with booking lifecycle events: generate on confirmed payment, hold on pending invoices, issue on waitlist promotion, and revoke on refund, cancellation, or transfer according to policy. Automatically reissues tokens on reschedule or venue change and sends attendee notifications. Ensures the displayed QR always mirrors real-time eligibility in ClassTap’s scheduling, invoicing, and attendee records.

Acceptance Criteria
Token Issuance on Confirmed Payment
Given a booking for event E has payment status Paid and attendee status Confirmed When the payment confirmation is recorded in ClassTap Then a RollingQR token bound to the attendee and current device is issued within 10 seconds And the QR becomes scannable for event E via EntryScan endpoints And only one active token exists per attendee-device-event (new issuance on another device invalidates the prior device token) And an audit log records token_id, event_id, attendee_id, issued_at, device_id
Hold on Pending Invoice (No Scannable QR)
Given a booking for event E has an Unpaid or Pending Invoice status When the attendee opens their pass in browser or wallet Then no scannable QR token is displayed (placeholder shown instead) And EntryScan validation for that attendee-event returns 402 Payment Required And the UI presents a Pay Now action that, upon successful payment, issues a token within 10 seconds
Waitlist Promotion Token Handling
Given an attendee is on the waitlist for event E When the attendee is promoted to a confirmed spot Then if auto-charge succeeds or invoice is Paid, a device-bound token is issued within 10 seconds And if payment is still Pending, the pass remains on Hold with no scannable QR And a notification is sent reflecting the new status (Issued or Hold)
Revocation on Refund, Cancellation, or Transfer
Given an attendee has an active token for event E When the booking status changes to Refunded, Cancelled, or Transferred Then the previously issued token is invalidated within 15 seconds And EntryScan returns a specific denial code: REFUNDED, CANCELLED, or TRANSFERRED And if transfer completes to a new attendee with valid payment status, a new token is issued to the recipient within 15 seconds And if partial refund occurs, token revocation follows the org policy flag revoke_on_partial_refund (True => revoke, False => keep)
Automatic Reissue on Reschedule or Venue Change
Given event E is updated with a new start time and/or venue while the attendee remains eligible When the change is saved in ClassTap Then a new token reflecting the updated event metadata is issued within 15 seconds And the previous token becomes invalid within 15 seconds And the attendee is notified of the update with the new details And EntryScan only accepts the latest token for the attendee-event
Real-Time Eligibility Mirror and Rolling Refresh
Given the attendee pass is open in browser or wallet When eligibility changes in ClassTap (e.g., payment posted, cancellation recorded) Then the displayed QR updates to the correct state within 10 seconds of the server change And active tokens rotate on a cadence of 5–15 seconds with TTL <= 30 seconds And EntryScan rejects tokens that are expired, revoked, event-mismatched, or device-mismatched with explicit error codes And attempts to scan a screenshot or forwarded image from another device fail with DEVICE_MISMATCH
Attendee Notifications for Lifecycle Events
Given lifecycle events occur for an attendee (Issued, Hold, Revoked, Reissued) When such an event is triggered Then the attendee receives a notification within 60 seconds via enabled channels (in-app, email, and push/SMS if configured) And the notification includes event name, date/time, venue, current pass state, and any required action (e.g., Pay Now) And duplicate notifications for the same event-state transition are suppressed within a 10-minute window
Admin Controls & Scan Analytics
"As an organizer, I want to configure rotation settings and monitor scan activity and fraud attempts so that I can balance speed, security, and operational needs."
Description

Adds admin settings for token TTL, rotation interval, allowed clock skew, offline validation window, and device-binding strictness. Provides real-time dashboards for check-in throughput, success/failure reasons, fraud/mismatch events, and no-show reduction, with filters by event, location, and instructor. Supports exports, role-based access control, and immutable audit logs for compliance and dispute resolution.

Acceptance Criteria
Configure Token TTL and Rotation Interval
Given an administrator with Manage RollingQR Settings permission, When they open Admin > RollingQR Settings, Then fields for Token TTL (seconds) and Rotation Interval (seconds) are visible with inline validation help Given Token TTL is between 15 and 180 seconds inclusive and Rotation Interval is between 5 and 120 seconds inclusive and Rotation Interval is less than Token TTL, When Save is clicked, Then the settings persist, a success confirmation is shown, and an audit entry is recorded with actor, old→new values, and timestamp Given any invalid combination (TTL < 15, TTL > 180, Rotation < 5, Rotation ≥ TTL), When Save is clicked, Then saving is blocked, field-level errors indicate the violated rule, and no settings change is applied Given settings are saved, When a QR token is issued after the save, Then it uses the new TTL and Rotation Interval; tokens issued prior continue honoring their previous values
Enforce Allowed Clock Skew at Check-in
Given Allowed Clock Skew S seconds is configured, When a valid token is scanned and the scanner device’s clock delta relative to server is ≤ S, Then the check-in does not fail for clock skew Given Allowed Clock Skew S seconds is configured, When a token is scanned and the scanner device’s clock delta is > S, Then the scan is rejected with reason code CLOCK_SKEW_EXCEEDED and a corrective message is shown to the operator Given scans are occurring, When clock-skew rejections happen, Then the dashboard’s “Failure Reasons” and “Clock Skew” counters reflect the events within 5 seconds and match raw event counts within 1% over a 15‑minute window Given a clock skew threshold change is saved, When subsequent scans occur, Then the new threshold is enforced within 60 seconds of save time
Offline Validation Window During Network Outage
Given an Offline Validation Window W minutes is configured (>0), When a scanner loses connectivity, Then it can provisionally validate tokens whose signed time is within W minutes and still within Token TTL, marking them as Offline-Provisional Given the scanner regains connectivity, When Offline-Provisional validations exist, Then they are synced and finalized within 30 seconds, de-duplicated if the attendee was later scanned online, and reflected in analytics Given a token older than W minutes or past TTL is scanned while offline, When validation is attempted, Then it is rejected with reason OFFLINE_WINDOW_EXPIRED or TOKEN_EXPIRED respectively Given offline activity occurred, When the dashboard is viewed, Then “Offline scans” and reconciliation counts are visible and match finalized totals within 1% after sync
Device-Binding Strictness Enforcement
Given Device-Binding Strictness is set to Strict, When a QR token originally bound to Device A is presented from Device B, Then the scan is rejected with reason DEVICE_MISMATCH and the event is counted under Fraud/Mismatch Given Device-Binding Strictness is set to Balanced, When a scan occurs with minor device attribute changes (e.g., browser minor version) but same device class, Then the scan passes; when a materially different device is used, Then it is rejected with DEVICE_MISMATCH Given Device-Binding Strictness is set to Relaxed, When a QR token is presented from a different device, Then the scan passes but is flagged as Suspected Forwarding in analytics Given any strictness setting changes, When the next scans occur, Then the new policy is enforced within 60 seconds and all mismatches are captured with device attributes in the audit log
Real-time Scan Dashboard Metrics and Reasons
Given live check-ins are occurring, When viewing the dashboard, Then scans/minute and queue throughput widgets update within 2 seconds of events and show a 1‑minute live rate and 15‑minute trend Given scans succeed or fail, When categorized by reason, Then the dashboard displays counts for OK, TOKEN_EXPIRED, ROTATION_STALE, CLOCK_SKEW_EXCEEDED, DEVICE_MISMATCH, DUPLICATE_SCAN, OFFLINE_WINDOW_EXPIRED within 2 seconds, with totals matching raw events within 1% over a rolling hour Given an event has started, When computing no-shows (attendees not checked in by event start + configurable 10‑minute grace), Then the No‑Show Reduction metric equals (baseline no‑show rate − current no‑show rate) ÷ baseline, where baseline is the average no‑show rate across the last 5 comparable occurrences of the same event series, and is displayed with the comparison window noted
Analytics Filtering and Export
Given analytics data exists, When filters for Event, Location, and Instructor are used (multi‑select), Then all widgets and tables apply the filters within 1 second and counts match a backend filtered query exactly Given filters are applied, When Export is requested, Then the exported file (CSV or JSON) contains only the filtered rows and columns: timestamp (ISO‑8601 UTC), event_id, event_name, location_id, location_name, instructor_id, outcome, reason_code, device_binding_status, and completes within 60 seconds for up to 100k rows Given an export completes, When the file is downloaded, Then column headers are present, encodings are UTF‑8, numeric IDs are preserved without scientific notation, and row counts equal the on‑screen total
Role-Based Access Control and Immutable Audit Logs
Given RBAC is configured, When a user with Admin role accesses RollingQR settings and analytics, Then access is allowed; when a user with Instructor role accesses analytics, Then only their assigned events are visible; when a user without privileges attempts access, Then a 403/Not Authorized response is returned and the attempt is logged Given any change to security settings or export initiation, When the action occurs, Then an audit log entry is appended capturing actor, action, before/after values (for settings), filters (for export), timestamp, and outcome Given audit logs exist, When attempting to edit or delete an entry via UI or API, Then the action is blocked and recorded as a separate audit event without altering the original entry Given audit logs are queried, When integrity is verified, Then each entry exposes a hash and previous_hash forming a chain; recomputing the chain over the last 1,000 entries matches stored values, demonstrating immutability
Accessibility & Offline Fallback
"As an attendee and staff, I want accessible, offline-capable check-in options so that entry still works when someone lacks a smartphone or connectivity is unreliable."
Description

Delivers inclusive and resilient entry alternatives: large/high-contrast QR display, screen-reader hints, haptic countdown, a 6-digit check-in PIN tied to the booking, printable passes for attendees without smartphones, and manual lookup workflows. Handles connectivity gaps by using cached last-valid tokens with a short grace window and scanner-side offline verification with later sync, ensuring check-in continuity.

Acceptance Criteria
Large/High-Contrast QR Display Toggle
Given an attendee views their RollingQR pass When they enable Large/High-Contrast mode or the system High Contrast setting is detected Then the QR renders at a minimum of 320x320 px with a quiet zone ≥ 4 modules and contrast ratio ≥ 7:1 And accompanying text and buttons render at ≥ 18 pt with line height ≥ 1.4 Given the attendee zooms up to 200% When the zoom level changes Then the QR remains fully visible without horizontal scroll and remains scannable by the first-party scanner at ≥ 95% success within 1 second under 300–500 lux lighting Given the attendee returns to the pass within the same session When the page reloads Then the Large/High-Contrast preference is persisted for that session
Screen Reader Guidance for Dynamic QR
Given a screen reader is active When the pass is opened Then a live region announces once: "Show this code to staff. It refreshes periodically. Use PIN if needed." And the QR image has alt text "Check-in QR code" Given the QR refreshes When a new token is rendered Then the live region politely announces "Code refreshed" no more than once per refresh cycle And no announcements occur more frequently than every 5 seconds Given the user navigates with a screen reader When moving focus Then the focus order is Event title -> Time/venue -> PIN -> QR -> Help link without trapping or unexpected jumps
Haptic Countdown Before QR Refresh
Given the device supports haptics and the user has enabled haptic feedback in pass settings When a QR token is within 3 seconds of refresh Then the device emits one short haptic tick at T-3, T-2, and T-1 seconds And no haptic is emitted if system haptics are disabled or Do Not Disturb is active Given the pass is in background or the screen is locked When a refresh occurs Then no haptic feedback is emitted
Six-Digit Check-in PIN Alternative
Given a confirmed booking When the attendee opens the pass Then a unique 6-digit PIN tied to the booking is displayed and cached for offline use Given a staff member enters the attendee’s PIN on the scanner When the PIN is valid and within the event check-in window (60 minutes before to 30 minutes after start) Then the attendee is marked Checked In and a success confirmation appears within 1 second Given repeated incorrect PIN submissions When 5 invalid attempts occur from the same device within 5 minutes Then further PIN entry is locked for 2 minutes and an error is logged Given the scanner has no connectivity When a valid PIN is entered Then the check-in is recorded as Provisional and will auto-sync within 60 seconds of reconnect
Printable Pass for Attendees Without Smartphones
Given an attendee selects "Get printable pass" from their confirmation When the PDF is generated Then it is a single-page A4/Letter PDF with tagged structure (PDF/UA), body text ≥ 14 pt, and contrast ratio ≥ 7:1 And it includes a scannable QR, attendee name, booking reference, event details, and the 6-digit PIN Given the printable pass QR is scanned at the door When using the first-party scanner Then validation succeeds at ≥ 95% success within 1 second under 300–500 lux lighting Given the QR on the print cannot be scanned When staff use the fallback information Then the attendee can be checked in using the printed 6-digit PIN within 1 second
Manual Attendee Lookup and Check-In
Given a staff member opens the event roster When they search by name, email, or booking reference Then results appear within 500 ms at p90 with diacritic-insensitive, typo-tolerant matching (Levenshtein distance ≤ 1 for names up to 12 characters) Given a result is selected When "Check in" is pressed Then the attendee status updates to Checked In with timestamp and device ID, and the action is undoable for 60 seconds Given the scanner is offline When manual check-in is performed Then the status is marked Provisional and syncs on reconnect with conflict resolution favoring the earliest check-in timestamp
Offline Grace Window Using Cached Last-Valid Tokens
Given the scanner loses connectivity When an attendee presents a token that was last validated online within the past 120 seconds and its signature verifies against cached keys Then entry is allowed and marked Provisional Given connectivity is restored When synchronization occurs Then provisional entries are confirmed or reverted within 60 seconds And any reverted attendee is flagged for re-validation at the gate Given the same attendee is checked in by two offline devices When sync resolves conflicts Then the earliest provisional check-in is kept and later ones are marked Duplicate with full audit logs

DeviceBind

Converts magic links into device‑specific passes using secure device fingerprinting and optional hardware attestation. Links only open on the enrolled device; attempts on other devices are denied or routed to a safe reissue flow—stopping link sharing without extra steps for legit clients.

Requirements

Cross-Platform Device Fingerprinting SDK
"As a client booking classes, I want my magic link to automatically recognize my device so that I can access my booking without extra steps and my link can’t be used by others."
Description

Provide a lightweight SDK for web (desktop and mobile browsers) and native mobile (iOS/Android) that computes a stable, privacy-preserving device identifier using multiple signals without collecting PII. The SDK exposes an enrollment function invoked on first magic-link open to register the device, stores a signed device key in secure storage (Keychain/Keystore/IndexedDB with fallbacks), and sends a cryptographic proof to the backend for binding. It must be resilient to app updates, browser restarts, and minor OS upgrades, while gracefully degrading in private/incognito mode. Integrates with ClassTap authentication to attach a device_id to session creation and with bookings/payments APIs to enforce device checks. Includes rate limiting, anti-replay protections, and versioned telemetry for troubleshooting.

Acceptance Criteria
First-Time Enrollment via Magic Link (Web & Mobile)
Given a valid ClassTap magic link and an unenrolled device, When the SDK initializes on link open, Then it computes a stable, non-PII device_id, generates a device keypair, stores the signed device key in secure storage (Keychain/Keystore/IndexedDB) or approved fallback, and posts a binding proof to the backend within 2 seconds. Given secure storage is unavailable, When enrollment occurs, Then the SDK uses an approved encrypted fallback store, marks storage_backend in versioned telemetry, and proceeds without user prompts. Given hardware attestation is enabled and supported on the platform, When enrollment occurs, Then the SDK obtains an attestation token and includes it in the binding proof; otherwise it sets attestation_status=unavailable and continues. Given enrollment succeeds, When any subsequent ClassTap magic link is opened on the same device, Then the SDK reuses the existing device_id without re-enrollment. Given enrollment succeeds, When a session is created, Then the SDK attaches device_id to the auth request and the backend confirms binding before issuing the session.
Identifier Stability Across Updates and Restarts
Given an enrolled device, When the app/browser restarts or is updated or the OS minor version increments, Then the computed device_id remains unchanged with ≥99.5% stability across the QA device/browser matrix (min 50 runs per platform). Given the browser clears cookies but retains IndexedDB, When the SDK initializes, Then device_id remains unchanged. Given device secure storage is cleared or the device is factory reset, When the SDK initializes, Then a new device_id is produced and telemetry emits reason=storage_reset without blocking normal enrollment.
Private/Incognito Mode Degradation and Safe Reissue Hook
Given the SDK runs in a private/incognito context, When enrollment is attempted, Then the SDK generates an ephemeral device_id, does not persist keys, sets device_id_ephemeral=true, and informs the backend which refuses binding. Given incognito is detected, When the app integrates the SDK, Then the SDK invokes onIncognitoDetected callback within 100 ms so the host app can route to a safe reissue flow. Given the user exits incognito and opens the link in a regular context, When the SDK initializes, Then normal enrollment completes within 2 seconds.
Security: Anti-Replay and Rate Limiting
Given an enrollment or assertion request, When the SDK sends a proof, Then the payload includes a signed nonce, timestamp, and device_id, and the backend rejects reused nonces or requests older than 60 seconds with HTTP 409; the SDK surfaces error_code=replay_detected. Given more than 5 enrollment attempts occur from the same IP or device_id within 10 minutes, When another attempt is made, Then the backend responds HTTP 429 and the SDK backs off exponentially up to 5 minutes and logs telemetry event=rl_blocked. Given a man-in-the-middle replays a prior proof, When the server verifies the signature, Then verification fails and no binding or session is created.
Privacy-Preserving Fingerprint (No PII)
Given SDK initialization, When computing device_id, Then only non-PII signals are used (e.g., OS version bucket, hardware class, browser engine, clock skew) and no personally identifiable information (e.g., name, email, phone, advertising ID, precise location, contacts) is collected or transmitted. Given identifiers are derived, When signals are transmitted, Then all raw values are locally normalized and salted+hashed; no raw device serials, MACs, or advertising IDs leave the device. Given a compliance review, When exporting the data dictionary, Then the SDK produces a machine-readable schema documenting fields, purposes, retention, and confirms no PII collection. Given telemetry is enabled, When events are sent, Then they are versioned, sampled at 10% by default (configurable), and redact values longer than 64 bytes.
Auth and API Device Enforcement Integration
Given a bound device opens a magic link, When the SDK requests a session via ClassTap auth, Then the request includes device_id and a fresh device assertion and the server issues a session with device_binding_status=bound. Given an unbound or mismatched device calls bookings or payments APIs, When the SDK includes device headers (X-Device-ID, X-Device-Assertion), Then the server returns 403 reason=device_mismatch and the SDK invokes onDeviceMismatch callback within 200 ms. Given a properly bound device calls bookings or payments APIs, When headers are validated, Then requests succeed with no additional user steps.
Storage Backends, Fallbacks, and Key Migration
Given iOS native, When storing the device key, Then the SDK uses Keychain with kSecAttrAccessibleAfterFirstUnlock and Secure Enclave when available; on Android it uses Keystore with StrongBox when available; on Web it uses IndexedDB + CryptoKey; if unavailable, it falls back to an AES-wrapped key in localStorage. Given storage write fails, When the SDK retries, Then it performs up to 3 retries with jitter (50–250 ms), and if still failing, it aborts enrollment with a recoverable error and emits telemetry event=storage_write_failed. Given the SDK version upgrades, When initialization runs, Then any key namespace migration occurs transparently without user prompts and without changing device_id (0 changes across 1,000 migration test runs).
Optional Hardware Attestation Support
"As a studio owner, I want stronger device verification for high-demand classes so that link sharing is deterred without adding steps for legitimate clients."
Description

Integrate platform attestation services to strengthen device binding where available: Apple App Attest/DeviceCheck for iOS, Google Play Integrity for Android, and WebAuthn device-bound key attestation for modern browsers. The system verifies attestation server-side and couples the attested key with the device pass, falling back to fingerprint-only when attestation is unavailable. Implement configuration flags to enable per-tenant or per-campaign enforcement levels (required, preferred, off). Cache and rotate attestation as needed and surface failures to observability dashboards. Ensure minimal user friction and low-latency verification paths using asynchronous attestation renewal.

Acceptance Criteria
Cross-Platform Attestation Verification
Given an iOS device with App Attest available and an enrollment challenge issued When the client submits a valid App Attest assertion and a newly generated device-bound public key Then the server verifies the assertion with Apple, binds the key to the device pass, and returns 201 Created Given an Android device with Google Play Integrity available and an enrollment challenge issued When the client submits a valid Integrity verdict and a newly generated device-bound public key Then the server verifies the verdict, binds the key to the device pass, and returns 201 Created Given a browser supporting WebAuthn attestation and an enrollment challenge issued When the user creates a device-bound credential and submits the attestation object Then the server verifies the attestation per policy, binds the credential to the device pass, and returns 201 Created
Enforcement Levels Behavior
Given tenant enforcement=required on a platform that supports attestation When enrollment occurs without a valid attestation Then the server rejects with 403 Attestation Required and offers a safe reissue link Given tenant enforcement=required on a platform that does not support attestation When enrollment occurs Then the server accepts fingerprint-only binding and logs enforcement_downgrade=true Given tenant enforcement=preferred When attestation is unavailable or verification fails Then the server completes fingerprint-only binding, tags attestation_mitigated=true, and emits a metric Given tenant enforcement=off When enrollment occurs Then attestation is skipped and fingerprint-only binding is completed Given a campaign-level enforcement override exists When a pass is created under that campaign Then the campaign enforcement overrides the tenant default
Fallback and Safe Reissue Flow
Given a magic link is opened on a device without the bound attested key but matching fingerprint When enforcement is preferred or off Then access is allowed and a background attestation enrollment prompt is queued Given a magic link is opened on a device that neither presents the bound attested key nor matches fingerprint When enforcement is required on a supported platform Then access is denied with 401 Device Not Enrolled and the user is routed to the safe reissue flow with one-time verification Given repeated reissue attempts exceed the default limit of 3 per hour per account When the safe reissue flow is invoked again within the window Then the system responds 429 Too Many Requests and emits a security notification event
Attestation Caching and Rotation
Given a device pass with a previously validated attestation within the configured cache TTL When the device opens a magic link Then the server uses the cached validation and does not call the remote attestation provider Given the cached attestation is within the configured renewal window before expiry When the device is active Then the client triggers asynchronous renewal and the server validates it without blocking the current request Given a cached attestation is expired and no renewal is available When enforcement is required on a supported platform Then access is denied and re-enrollment is prompted Given the device rotates its attested key and submits a valid new attestation When rotation is processed Then the server atomically updates the bound key and revokes the previous key for that pass
Low-Latency Verification and Minimal Friction
Given a cached valid attestation or a fingerprint-only path When a user opens a magic link Then p95 DeviceBind-added authorization latency is <= 200 ms in-region Given a fresh attestation verification is required (no usable cache) When the user opens a magic link Then p95 added latency from attestation verification is <= 600 ms and UI presents a single non-blocking spinner Given asynchronous attestation renewal is triggered When renewal runs Then the user is not interrupted and the operation succeeds >= 99% within 30 seconds or retries with exponential backoff up to 3 times Given the client is offline during a renewal window When a magic link is opened Then access proceeds if a non-expired cached attestation or fingerprint-only path is valid per enforcement and renewal is deferred
Observability and Alerting
Given attestation operations occur When metrics are emitted Then dashboards show success rate, failure rate, fallback rate, p50/p95/p99 verification latency, cache hit rate, and enforcement overrides by platform and tenant/campaign Given attestation verification failures exceed 2% over 5 minutes for any platform-region-tenant tuple When the threshold is crossed Then a high-severity alert is sent to on-call with links to logs and dashboards Given 401/403 responses due to enforcement are generated When events are logged Then logs include correlation ID, tenant ID, campaign ID, enforcement level, platform, hashed device identifier, and reason code with PII redacted Given attestation provider API errors exceed 1% over 10 minutes When detected Then the circuit breaker opens, the system falls back per enforcement policy, and an alert is emitted
Replay and Integrity Protections
Given an enrollment challenge nonce is issued When an attestation is submitted with a stale or mismatched nonce Then the server rejects with 400 Invalid Nonce and logs a security event Given an attestation blob is replayed for a different challenge or device When verification runs Then the server rejects it and temporarily locks further attestation from the source for a configurable cooldown Given platform signals indicate a compromised device (rooted/jailbroken/compromised integrity) When enforcement is required or preferred Then the server denies or downgrades to fingerprint-only per enforcement and flags the pass for review Given a WebAuthn attestation is returned with an unacceptable format or untrusted root per policy When verification runs Then registration proceeds without attestation under preferred/off or is rejected under required, and the decision is recorded
Magic Link Pass Binding & Token Lifecycle
"As a client, I want my link to turn into secure access on my phone so that I don’t have to log in and others can’t use my link."
Description

Convert single-use magic links into device-bound access passes at first open. On initial click, exchange the link token for a signed, device-scoped pass tied to the enrolled device key; invalidate the original link token immediately. Store the pass securely, auto-refresh it via short-lived rotating tokens, and require the device proof on each privileged action such as viewing booking details, presenting check-in QR, or completing payment follow-ups. Implement idempotent enrollment to handle double-taps and network retries, along with clock-skew tolerant expirations. Provide server-side enforcement middleware for all endpoints that previously trusted magic links, returning standardized error codes for non-matching devices.

Acceptance Criteria
First Open Converts Magic Link to Device‑Bound Pass
Given a valid, unconsumed magic link token L for user U And Device A has generated a device keypair and (if available) hardware attestation When U opens L on Device A Then the server exchanges L for a signed device‑bound pass P linked to Device A’s public key (and attestation if present) And the response includes P with an expiry set to the configured short TTL and a refresh mechanism And P is marked active for U and Device A And an audit event "pass_issued" is recorded with token ID, device ID, and timestamp
Single‑Use Link Token Immediate Invalidation
Given magic link token L has been exchanged for pass P When any client attempts to reuse L on any device Then the server does not issue a new pass and responds with a standardized error (HTTP 410 or 401 with code CT_LINK_EXHAUSTED) And the attempt is audit‑logged without revealing sensitive data
Idempotent Enrollment on Double‑Tap and Network Retries
Given token L was exchanged for pass P for Device A And the client repeats the exchange due to double‑tap or retry within the configured idempotency window When the server receives the repeated request from the same device fingerprint/attestation Then the server returns the same active pass P (or equivalent metadata) without creating duplicate passes or side effects And the response indicates idempotent replay via headers or metadata
Secure Client‑Side Pass Storage
Given pass P is issued to a client When the client persists P Then P is stored only in platform‑secure storage (e.g., iOS Keychain / Android Keystore; Web via HttpOnly, Secure, SameSite=strict cookie) And P is never written to URL/query params, localStorage/sessionStorage, or application logs And on logout or server‑initiated revocation, P is removed from client storage promptly and not sent on subsequent requests And access to P is protected by OS‑level controls where supported
Rotating Token Auto‑Refresh with Clock‑Skew Tolerance
Given an active pass P with expiry Te and refresh capability R When the client requests refresh within the configured pre‑expiry window or within the configured post‑expiry skew window Then the server issues a new pass P2 and revokes P upon successful rotation And any concurrent refresh attempts result in a single valid rotated pass (idempotent) And refresh requests with invalid/absent device proof are rejected with code CT_DEVICE_PROOF_INVALID And replayed or expired refresh artifacts are rejected with code CT_TOKEN_EXPIRED
Device Proof Required for Privileged Actions
Given Device A holds an active pass P for user U When the client calls a privileged endpoint (booking details, check‑in QR, payment follow‑up) Then the request includes a fresh, nonce‑based device proof bound to P and Device A’s key And the server verifies nonce freshness, signature validity, and binding to U and Device A And on success the endpoint proceeds; on failure respond with standardized errors: CT_DEVICE_PROOF_MISSING, CT_DEVICE_MISMATCH, or CT_TOKEN_EXPIRED And failures are audit‑logged with timestamp, endpoint, device ID (hashed), and error code
Middleware Enforcement and Standardized Error Responses
Given endpoints that previously accepted magic links are listed in enforcement configuration When requests target any listed endpoint Then enforcement middleware executes before handlers and requires a valid active device‑bound pass and proof And 100% of listed endpoints are covered by automated tests asserting middleware enforcement And responses use standardized schema and HTTP codes: 401 CT_AUTH_REQUIRED (missing/expired), 403 CT_DEVICE_MISMATCH (wrong device), 410 CT_LINK_EXHAUSTED (reused link) And middleware emits metrics for allow/deny counts and error codes for observability
Cross-Device Attempt Handling & Safe Reissue Flow
"As a client who changed phones, I want a secure way to move my access to my new device so that I can still attend class without contacting support."
Description

Detect and block access when a magic link or bound pass is opened on a non-enrolled device. Present a safe reissue flow that verifies identity via primary channel (email/SMS OTP) and obtains explicit confirmation to transfer or add a device according to policy. Log the event, notify the original device, and throttle reissue attempts to mitigate abuse. Provide a human-friendly path for staff to assist clients with identity verification and reissue codes. Ensure all denial and reissue responses are localized, accessible, and consistent across web and mobile.

Acceptance Criteria
Block Access on Non-Enrolled Device
- Given a magic link or bound pass enrolled on Device A, When opened on Device B, Then access is denied (HTTP 403 or equivalent) and a "Verify your identity to reissue" action is displayed. - The original link/pass remains usable on Device A with no state consumed or invalidated. - An audit event "cross_device_denied" is recorded with fields: user_id, pass_id, link_id, detected_device_fingerprint, enrolled_device_fingerprint, timestamp (UTC), ip, approx_geo, user_agent, reason="device_mismatch". - The denial UI does not disclose whether a specific email/phone exists; messaging is generic and privacy-preserving. - Denial content is localized according to user preference or Accept-Language and matches the platform’s style guide.
Safe Reissue via Primary Channel OTP
- Given the user selects "Verify identity", When the primary channel is email or SMS, Then a 6-digit numeric OTP is sent to the primary contact; OTP TTL = 10 minutes; single-use. - OTP sends are rate-limited to max 3 per 60 minutes per user per channel and no more than 1 per 30 seconds. - When the correct OTP is submitted within TTL, Then the user is presented with options per policy: "Transfer to this device" and/or "Add this device" with a summary of consequences. - Upon confirmation, the binding updates server-side; the new device gains access and the prior device’s access updates per policy within 5 seconds. - An audit event "reissue_success" is recorded with user_id, pass_id, channel (email|sms), device_fingerprint, ip, timestamp, action (transfer|add).
Reissue Attempt Throttling and Abuse Mitigation
- The system enforces a maximum of 5 OTP verification attempts per rolling 24 hours per user; excess attempts are blocked. - After 3 consecutive failed OTP submissions, submissions are locked for 1 minute; after 4, locked for 5 minutes; after 5+, locked for 30 minutes (per user and device). - Throttling/lockout events are logged with user_id, device_fingerprint, reason, and next_allowed_at (UTC). - User-facing messages indicate a temporary lockout and the next allowed time without revealing whether contact details are valid. - Elevated risk signals (e.g., high-velocity IP reuse, country mismatch, emulator detection) trigger escalation to staff-assisted reissue and are logged as "risk_escalation".
Original Device Notification on Cross-Device Events
- On any cross-device denial, reissue start, or reissue completion, a notification is sent to the original enrolled device within 30 seconds containing event type and timestamp. - If push/in-app delivery is unavailable or fails, the system falls back to the user’s configured email/SMS preference. - The notification includes a link/action to review devices that is only actionable from an enrolled device; it is blocked on non-enrolled devices. - Notification copy is localized to the user’s preferred language and uses shared message templates across web and mobile.
Localization and Accessibility of Denial & Reissue
- All denial and reissue screens/messages have translations for 100% of supported locales; missing keys fail CI checks and block release. - Language is selected by user preference, else Accept-Language header, else product default; the same string keys are consumed on web and mobile from a shared resource bundle. - UI meets WCAG 2.1 AA: color contrast >= 4.5:1, visible focus, keyboard navigable controls, labeled form fields, ARIA-live announcements for errors/success. - OTP input supports paste and platform autofill (including SMS OTP on mobile) and is announced correctly by screen readers. - Visual and copy consistency is validated via snapshot tests comparing web and mobile outputs per locale.
Staff-Assisted Reissue Path
- Staff console access to reissue requires role=Support and permission=can_reissue_device and is protected by staff 2FA. - Staff can locate a client by email, phone, or booking ID and issue an 8-character alphanumeric reissue code; TTL = 10 minutes; single-use; max 2 staff-issued codes per client per 24 hours. - Before issuing, staff must select an identity verification method from a predefined list and provide a note (minimum 10 characters); the action is blocked until completed. - An audit event "staff_reissue" is recorded with staff_id, client_id, reason_code, note, ip, timestamp; PII is minimized per policy. - The original device owner is notified of staff-assisted changes within 30 seconds via preferred channel.
Policy Enforcement for Transfer vs Add Device
- If policy is one-device-per-pass, only "Transfer" is offered; confirmation explicitly warns the current device will lose access; user must confirm to proceed. - If policy allows up to N devices, "Add" is offered only when current_device_count < N; the UI shows remaining slots (N - current_device_count). - Binding updates are atomic; concurrent requests do not result in exceeding policy limits; conflicts return a localized retry message. - If a cooldown applies (e.g., min 24 hours between transfers), the UI blocks reissue and displays the next eligible time. - All policy decisions are enforced server-side; client-side attempts to bypass options are ignored and logged as "policy_violation".
Device Change & Recovery UX
"As a frequent client, I want an easy, secure path to recover access if I lose or reset my phone so that I don’t miss booked classes."
Description

Offer a guided flow for legitimate device changes, lost device scenarios, and browser resets that minimizes friction while maintaining security. Support temporary grace windows after successful identity verification, single-use recovery codes, and studio-configurable limits on the number of transfers per booking or per month. Provide clear messaging in emails/SMS and in-app screens, with progress indicators and expected timelines. Include fallback for clients in regions with unreliable SMS by prioritizing email or authenticator-based verification where available.

Acceptance Criteria
Verified Device Change with Temporary Grace Window
Given a client passes identity verification for an active booking When they initiate a device change Then the system issues a configurable grace window (default 30 minutes, range 5–60) during which the new device can be enrolled Given a grace window is active When the client opens the magic link on the verified new device Then the link opens, the booking is bound to the new device, and the old device is immediately invalidated for that booking Given the grace window has expired When the client attempts to enroll a device Then the attempt is rejected and the user is prompted to restart verification; no partial bindings occur Given a device change completes Then an audit record is stored with timestamp, booking ID, hashed old/new device fingerprints, and actor; viewable by studio admins Rule: Only one active grace window per booking per client at any time; subsequent requests revoke the prior window Rule: Any access from unverified devices during the grace window is routed to the safe reissue flow without revealing enrollment status
Lost Device Recovery via Single-Use Recovery Code
Given the client has generated recovery codes previously When they select “Use recovery code” in the recovery flow and enter a valid unused code Then the device rebind flow starts and the entered code is immediately invalidated Given a recovery code is invalid, already used, or expired When the client submits it 5 times cumulatively within 15 minutes Then further attempts are blocked for 15 minutes and a notification is sent to the client’s email Given recovery completes successfully Then all sessions and device bindings on the previous device are revoked, and any waitlist-to-payment tokens tied to the old device are invalidated Rule: Recovery codes are single-use, 10 codes per set, rotatable by the client; generating a new set invalidates all prior codes Rule: Codes have a maximum lifetime of 12 months and are stored hashed server-side; last 4 characters are shown only at generation time
Browser Reset Rebind on Same Device
Given the client is on the same physical device with cleared cookies or a new browser When they open a valid magic link Then the system detects device fingerprint continuity and offers a low-friction rebind that does not count toward transfer limits Given the low-friction rebind prompt is shown When the client verifies via email link or authenticator approval Then access is restored and the browser is bound to the booking Given device fingerprint mismatch exceeds threshold When a magic link is opened Then the full device change flow is required and the action counts as a transfer SLO: Median time to complete same-device rebind < 30 seconds; 95th percentile < 90 seconds measured in staging and production telemetry
Studio-Configurable Transfer Limits Enforcement
Given studio settings define per-booking limit N and per-month per-client limit M (defaults N=1, M=3) When a client initiates a device change Then the stricter of N and M is enforced and the attempt proceeds only if within limits Given the client has reached the applicable limit When they attempt another transfer Then the system blocks the transfer, shows an explanatory message, and provides a path to contact studio support or request an admin override Given an authorized admin applies a one-time override with reason When the client retries the transfer Then the transfer proceeds once and the override is recorded in audit logs with admin ID, reason, and timestamp Rule: Limit configuration is validated in admin UI, is effective immediately for new requests, and all changes are audit-logged
Clear Multi-Channel Messaging with Progress Indicators
Rule: All device change/recovery emails, SMS, and in-app screens include context (why the message was sent), next-step CTA, a progress stepper (min 3 steps), an estimated completion time, and a support link Rule: Messages must not expose device fingerprints or code fragments; only show the last 2 characters of the booking ID for reference Rule: Accessibility meets WCAG 2.1 AA for contrast and focus order; content is localized for supported locales; SMS core content fits within 160 characters plus a short link Rule: Transactional emails are DKIM-signed; links carry short-lived tokens (≤ 15 minutes) and deep-link into the app; expired links display a safe reissue option with no sensitive leakage
SMS-Fallback to Email/Authenticator in Unreliable Regions
Given the client’s country is on the SMS-unreliable list or a delivery failure/timeout is detected When initiating verification during recovery or device change Then the system prioritizes email or authenticator push/OTP and informs the client of the selected channel Given the client has both email and authenticator available When fallback is triggered Then the client can choose a preferred channel and that preference is remembered for future recoveries SLO: In controlled tests for affected regions, end-to-end verification success rate with fallback ≥ 98% over a rolling 7-day period Rule: All channels enforce equivalent security controls (rate limiting, replay protection, device-bound tokens) and are fully audit-logged
Policy Controls & Audit Analytics
"As a studio owner, I want to configure device enforcement and see its impact so that I can balance security with customer experience and measure ROI."
Description

Add tenant-level controls in the ClassTap dashboard to configure DeviceBind behavior: enforcement level, maximum devices per client, reissue rules, attestation preference, and exception lists such as staff devices or kiosks. Provide real-time analytics and exportable audit logs showing enrollments, denials, reissues, suspected sharing prevented, and impact on no-shows and revenue. Integrate with existing reporting and webhooks to notify studios of suspicious patterns. Ensure logs are tamper-evident and retention complies with data policies.

Acceptance Criteria
Tenant Admin Configures DeviceBind Policy Controls
Given a tenant admin on the ClassTap dashboard, When they open Settings > DeviceBind Policy, Then they can view and edit enforcement_level [Off|Monitor|Enforce], max_devices_per_client (integer 1-5), reissue_rule [Auto|RequireApproval], attestation_preference [Off|Optional|Required], and manage exception lists. Given valid changes, When Save is clicked, Then settings persist, page reload shows saved values, an audit event policy_change is recorded with old and new values, and policies take effect within 60 seconds. Given a non-admin user, When they attempt to access DeviceBind Policy, Then access is denied (HTTP 403 or UI blocked) and an access_denied audit event is logged.
Enforcement Levels and Max Devices Behavior
Given enforcement_level=Enforce and max_devices_per_client=1, And a client has an enrolled device, When the client opens a valid magic link on a second device, Then access is denied and the user is routed to the reissue flow, And an audit event denial is logged with reason_code=max_devices_exceeded. Given enforcement_level=Monitor and max_devices_per_client=1, When the client opens a valid magic link on a second device, Then access is allowed, And an audit event monitor_only is logged with would_deny=true and reason_code=max_devices_exceeded. Given enforcement_level=Off, When the client opens a valid magic link on any device, Then access is allowed and no enforcement occurs, And an audit event enforcement_disabled is logged. Given reissue_rule=RequireApproval, When a reissue is requested, Then the request enters Pending status and no device is re-bound until approved by a tenant admin, And approval/denial actions are logged.
Attestation Preference Handling
Given attestation_preference=Required and the device supports attestation, When enrollment occurs and attestation passes, Then the device is bound and an audit event enrollment is logged with attestation=passed. Given attestation_preference=Required and the device does not support or fails attestation, When enrollment is attempted, Then binding is blocked and the user is routed to the reissue or alternative verification flow, And an audit event denial is logged with reason_code=attestation_required_not_met. Given attestation_preference=Optional, When enrollment occurs on a device without attestation, Then binding succeeds with attestation=not_supported_or_skipped and an audit event is logged. Given attestation_preference=Off, When enrollment occurs, Then no attestation challenge is performed and attestation=disabled is logged.
Exception Lists for Staff and Kiosk Devices
Given a device fingerprint is added to the tenant's Staff Exceptions, When any client opens a magic link on that device, Then enforcement checks are bypassed for that session, the client is not counted against max_devices, And an audit event exception_bypass is logged with exception_type=staff. Given a device fingerprint is added as a Kiosk exception, When clients open magic links on that device, Then enforcement checks are bypassed, And events are tagged exception_type=kiosk. Given an exception entry with an expiration timestamp, When the timestamp passes, Then the exception no longer applies within 60 seconds and enforcement resumes. Given an exception entry is removed by an admin, When the change is saved, Then the exception stops applying within 60 seconds and an audit event policy_change is logged.
Real-Time Analytics and Reporting Integration
Given DeviceBind is active, When enrollments, denials, and reissues occur, Then the analytics dashboard displays metrics enrollments_count, denials_count, reissues_count, suspected_sharing_prevented_count, no_show_rate_delta, and revenue_delta with event-to-UI latency <= 60 seconds. Given filter selections (date range, class, location), When applied, Then all metrics and charts recompute to the filtered scope within 5 seconds. Given existing reporting pages and exports, When accessed, Then DeviceBind metrics and dimensions are available and accurate, matching the analytics dashboard for the same filters.
Audit Logs Export and Tamper Evidence
Given a tenant admin selects a date range, When exporting audit logs, Then CSV and JSON downloads are available with fields: timestamp_utc, tenant_id, client_id, event_type, device_id_hash, ip, geo_country, reason_code, performed_by, request_id, integrity_hash, prev_hash. Given an exported batch, When the integrity API /audit/verify is called with range and head_hash, Then it returns verified for unmodified sequences and mismatch if any record is altered or missing. Given audit logs are append-only, When attempting to update or delete a specific event via API or UI, Then the operation is rejected and an access_denied attempt is logged. Given the tenant's Data Retention Policy is set to 90 days, When the system time advances beyond 90 days for existing events, Then those events are no longer retrievable via UI, API, or export within 24 hours and pruning is logged with summary counts.
Webhooks for Suspicious Patterns
Given 3 or more denied attempts for the same client from 3 distinct device fingerprints occur within 6 hours, When the threshold is met, Then a devicebind.suspicious_activity webhook is sent to the tenant's configured endpoint within 60 seconds. Given webhook delivery, When the receiver verifies the X-CT-Signature using the tenant's webhook secret, Then the signature matches the request body (HMAC-SHA256). Given transient delivery failures (HTTP 5xx or timeout), When sending the webhook, Then retries occur with exponential backoff for up to 24 hours and delivery attempts are logged with final status delivered or failed. Given the webhook payload, When inspected, Then it includes tenant_id, client_id, pattern=multiple_denials_distinct_devices, counts, first_seen, last_seen, sample_device_hashes (redacted), and correlation_id.
Security, Privacy & Compliance Guardrails
"As a compliance manager for a studio, I want assurance that device binding respects privacy laws so that we can deploy it without regulatory risk."
Description

Enforce PII-minimizing data practices by avoiding collection of raw identifiers and hashing/salting any device signals. Document data flows and retention periods, and provide DSAR/erase hooks to purge device identifiers per user request. Conduct threat modeling for token theft, replay, and downgrade attacks; implement HSTS, TLS pinning on mobile, nonce-based proofs, and signed JWT passes with audience and expiration claims. Ensure regional compliance such as GDPR/CCPA, accessibility compliance for user-facing flows, and provide a configurable data residency option for EU tenants.

Acceptance Criteria
No Raw Identifiers & Hashed Device Signals Enforced
Given DeviceBind processes device signals for pass enrollment When a device registers or authenticates Then no raw device identifiers (e.g., IMEI, MAC, IDFA, Android ID) are persisted in databases, caches, analytics, or logs And any stored device signal derivatives are produced via a one-way hash (e.g., Argon2id) with a per-tenant salt and a KMS/HSM-protected pepper And application and data-access logs redact or tokenize restricted identifiers prior to export And CI/CD static checks prevent adding restricted identifier fields to persistence schemas And a privacy review checklist for the change set is completed and approved
Documented Data Flows, Retention, Automated Purge, and Threat Model Sign-off
Given a release containing DeviceBind changes is prepared When the release candidate is created Then data flow diagrams and a record of processing activities (ROPA) for DeviceBind are updated in the repo and linked from the runbook And each data store holding device-derived identifiers has a documented retention period and legal basis And automated jobs purge device-derived identifiers after 365 days of inactivity unless under legal hold And purges include indices, caches, analytics exports, and backups within 30 days of primary deletion And purge outcomes are logged and alerted on failure And a threat model covering token theft, replay, and downgrade attacks is reviewed and signed off prior to release
DSAR Access/Erase Purges Device Identifiers Within SLA
Given a verified DSAR access or erase request is submitted for a user When the request is executed via admin UI or API Then the system locates and deletes device-derived identifiers, linkage metadata, and related audit references for that user across all stores within 30 days And downstream processors are notified via webhook within 24 hours to perform corresponding erasures And the user’s device-specific passes are invalidated and cannot be used post-erasure And a machine-readable erasure report is generated including data categories, systems touched, and timestamps And the operation is idempotent and safe if data is already absent And audit logs capture request metadata without storing additional PII beyond the user identifier and timestamps
Replay/Downgrade Protections via Nonce Proofs and Versioning
Given a magic link is exchanged for a device-specific pass When the client submits the proof payload Then the server requires a one-time nonce bound to the link ID, device fingerprint, and protocol version And nonces expire after 5 minutes and cannot be reused; reuse attempts return HTTP 409 and are audit logged And minimum supported protocol/app version is enforced; downgrade attempts (e.g., omitting required attestation on capable devices) return HTTP 426 with remediation instructions And proof submissions are rate-limited to 5 per minute per link and device with exponential backoff And integrity of proof payloads is verified (nonce included in signed payload); tampering leads to HTTP 400 and audit log entry
Token and Transport Security Controls Implemented
Given DeviceBind issues and validates device-specific passes When a pass is generated or presented Then passes are signed JWTs using ES256 with kid referencing an active key; claims include aud (tenant), exp (<= 24h), iat, and unique jti And validation rejects tokens with bad signature, wrong aud, expired exp (allowing 60s clock skew), unknown kid, or replayed jti And signing keys are stored in KMS/HSM; a JWKS endpoint serves active public keys with cache headers; key rotation is tested to avoid downtime And web endpoints serve over TLS 1.2+ with strong ciphers and HSTS enabled with includeSubDomains and preload; SSL Labs grade is A or better And mobile SDK performs certificate/public-key pinning with at least two pins (current and next) and fails closed on pin mismatch; rotation is validated in staging
Regional Data Residency for EU Tenants
Given a tenant is configured for EU data residency When DeviceBind stores or processes device-related data for that tenant Then all at-rest storage, caches, telemetry, and backups reside in EU regions And processing is executed by EU-scoped services; cross-region transfers are blocked by policy and network controls And exceptions require explicit tenant opt-in and DPA updates; all transfers are logged and reportable And disaster recovery and failover remain within EU regions and are tested at least annually And residency status is visible in admin settings and exportable in a compliance report
Accessibility Compliance for Safe Reissue and User-Facing Flows
Given a user interacts with magic link landing and safe reissue flows When using keyboard-only navigation or screen readers on supported browsers and mobile Then flows meet WCAG 2.2 AA: semantic landmarks, focus order, visible focus, labels/ARIA for controls, and color contrast >= 4.5:1 And error states are programmatically associated with inputs and include actionable guidance And time-limited steps (e.g., nonce expiry) provide warnings and an accessible path to reissue without loss of context And all interactive elements are operable without pointer gestures; motion or hardware attestation prompts have non-motion alternatives where applicable And accessibility is verified with automated tooling and manual assistive tech testing (NVDA/JAWS/VoiceOver/TalkBack)

GeoFence Gate

Accepts scans only within a configurable radius and time window around the venue. Prevents remote buddy check‑ins and ensures attendance reflects who actually arrived—improving payroll accuracy, capacity control, and compliance for pop‑ups and multi‑room studios.

Requirements

Configurable GeoFence Policy Engine
"As a studio owner, I want to define per-class and per-venue geofence rules so that only attendees physically at the location can check in and my payroll and capacity reports are accurate."
Description

Provide an organization- and venue-level policy engine to define check-in geofence rules, including default radius (in meters), class-level overrides, and policy templates by class type. The engine evaluates a user’s location at scan time and permits check-in only when the device is within the configured boundary for the scheduled class. It integrates with ClassTap’s scheduling, QR/scan check-in, and capacity tracking to ensure attendance reflects on-site presence, reduce double-bookings, and improve payroll accuracy. Admins can enable hard-block or warn-only behavior, configure error tolerance based on GPS accuracy, and localize messages to attendees and staff.

Acceptance Criteria
Default Radius Applied at Organization and Venue Levels
Given organization default radius=150m and venue override radius=100m and no class override When an attendee scans 95m from the venue point during the valid check-in time window Then check-in is permitted and recorded with policy_scope="venue" and effective_radius=100m Given the same configuration When an attendee scans 120m from the venue point Then check-in is denied with code="GEOFENCE_OUT_OF_BOUNDS", effective_radius=100m, distance_recorded=120m, and no attendance record is created Given venue override is removed and organization default radius=150m When an attendee scans 145m from the venue point during the valid time window Then check-in is permitted with policy_scope="organization" and effective_radius=150m
Class-Level Radius Override via Policy Template
Given class type "Outdoor Bootcamp" is mapped to policy template radius=300m and the class instance sets an override radius=200m When an attendee scans 190m from the venue point during the valid time window Then check-in is permitted with policy_scope="class_override" and effective_radius=200m Given the same class instance When an attendee scans 250m from the venue point Then check-in is denied with code="GEOFENCE_OUT_OF_BOUNDS" and effective_radius=200m Given class type "Yoga" uses template radius=100m with no class override When an attendee scans 90m from the venue point during the valid time window Then check-in is permitted with policy_scope="template" and policy_template_id recorded
Hard-Block and Warn-Only Enforcement Modes
Given enforcement_mode="HARD_BLOCK" and attendee is outside the effective radius by any distance When scanning for check-in Then check-in is denied with code="GEOFENCE_OUT_OF_BOUNDS", an audit entry is recorded with decision="deny", and no attendance or capacity mutation occurs Given enforcement_mode="WARN_ONLY" and attendee is outside the effective radius When scanning for check-in Then check-in is permitted, a visible warning message is shown to the scanner and attendee, the attendance record is flagged geofence_warning=true, and an audit entry is recorded with decision="allow_with_warning" Given enforcement_mode="WARN_ONLY" and attendee is inside the effective radius When scanning for check-in Then check-in is permitted with no warning and geofence_warning=false
GPS Accuracy Tolerance Handling
Given max_allowed_accuracy=30m and accuracy_handling="REQUIRE_ACCURACY" When reported GPS accuracy=45m at a reported distance=80m with effective_radius=100m Then check-in is denied with code="GPS_LOW_ACCURACY" and accuracy_recorded=45m Given max_allowed_accuracy=30m and accuracy_handling="ALLOW_WITH_WARNING" When reported GPS accuracy=45m at a reported distance=80m with effective_radius=100m Then check-in is permitted, a warning is shown, and attendance is flagged gps_accuracy_warning=true Given evaluation rule "permit if (reported_distance_m - reported_accuracy_m) <= effective_radius_m" When reported accuracy=20m, reported distance=110m, effective_radius=100m Then check-in is permitted and audit logs include reported_distance=110, reported_accuracy=20, effective_radius=100, rule_applied="distance_minus_accuracy" Given the same evaluation rule When reported accuracy=5m, reported distance=106m, effective_radius=100m Then check-in is denied with code="GEOFENCE_OUT_OF_BOUNDS" and evaluation inputs are logged
Check-in Time Window Enforcement
Given check-in_time_window_before_start=10 minutes and check-in_time_window_after_end=15 minutes and class scheduled 10:00–11:00 in the venue timezone When a scan occurs at 09:51 Then check-in is permitted Given the same configuration When a scan occurs at 09:49 Then check-in is denied with code="OUTSIDE_TIME_WINDOW" Given the same configuration When a scan occurs at 11:14 Then check-in is permitted Given the same configuration When a scan occurs at 11:16 Then check-in is denied with code="OUTSIDE_TIME_WINDOW" Given a device is set to a different timezone than the venue When evaluating the time window Then the venue timezone is used for the decision and is recorded in the audit log
Capacity Tracking and Double-Booking Prevention
Given class capacity=20 and current_checked_in=20 When an additional attendee scans within the geofence and within the time window Then check-in is denied with code="CLASS_FULL" and no attendance record is created Given double_booking_policy="BLOCK_OVERLAP" and the attendee has an existing checked-in attendance overlapping by at least 1 minute with this class When the attendee scans for this class Then check-in is denied with code="DOUBLE_BOOKED" and the conflicting class reference (class_id and times) is included in the message/log Given double_booking_policy="WARN_ONLY" and an overlapping attendance exists When the attendee scans for this class Then check-in is permitted and the attendance record is flagged double_booked_warning=true
Localized Attendee and Staff Messaging
Given user preferred locale="es-ES", venue default locale="en-US", and org default locale="en-GB" When a geofence denial occurs for being 37m outside a 100m radius Then the attendee-facing message is displayed in Spanish and includes localized numbers and units (e.g., "Estás a 37 m fuera del radio de 100 m"), and the staff-facing message is localized per staff user locale Given a translation key is missing in the user’s locale When any geofence warning or denial is generated Then the system falls back in order: user locale -> venue default -> organization default -> system default ("en-US") Given enforcement_mode="WARN_ONLY" and staff locale="fr-FR" When the attendee is outside the radius Then a single warning banner is shown with message key "geofence.warn_out_of_bounds" rendered in French and the message template variables (distance, radius) are interpolated and localized
Secure Location Verification & Anti-Spoofing
"As an operations manager, I want robust anti-spoofing during check-in so that remote or falsified scans cannot be used to bypass attendance rules."
Description

Implement multi-signal location verification that combines GPS, network-based location, and optional Bluetooth beacons to validate presence with accuracy thresholds. Detect and block mock-location providers, emulator usage, jailbreak/rooted devices, and time or token replay attempts. Use short-lived, signed scan tokens and server-side verification to ensure the scanning device and attendee account match the reservation. Record OS-reported accuracy and confidence to support risk scoring and enforcement decisions, minimizing remote buddy check-ins and fraud.

Acceptance Criteria
Accept scan within geofence and time window
Given a venue has geofence_radius_m, accuracy_threshold_m, confidence_threshold, and a check_in_window [start,end] configured And the attendee holds a valid reservation for the scheduled class When the attendee initiates a scan within geofence_radius_m of the venue centroid And current_time is within the check_in_window And OS-reported horizontal_accuracy_m <= accuracy_threshold_m And the multi-signal verifier computes confidence_score >= confidence_threshold Then the server accepts the scan and returns success And an attendance record is created linked to account_id, reservation_id, and device_id And the record stores lat, lon, horizontal_accuracy_m, confidence_score, signal_sources_used, device_id, and server_timestamp And subsequent scan attempts for the same reservation are rejected with ALREADY_CHECKED_IN
Reject scan outside geofence or time window
Given geofence_radius_m and check_in_window [start,end] are configured for the class When a scan is attempted with distance_to_venue > geofence_radius_m OR current_time outside [start,end] Then the server rejects the scan And returns error code GEO_OUT_OF_BOUNDS when outside radius And returns error code WINDOW_CLOSED when outside time window And no attendance record is created And a security event is logged with reason and context (distance, time, account_id, device_id)
Detect and block mock-location and spoofing
Given the client reports location_source metadata and device telemetry And mock-location provider or spoofing indicators are present (e.g., OS mock flag true, test provider, or known spoofing frameworks detected) When the attendee attempts to scan Then the server rejects the scan with error code LOCATION_SPOOFING_DETECTED And no attendance record is created And the device/account risk score is incremented per policy And a security event is logged including indicators, app_hash, and verification artifacts
Block rooted/jailbroken or emulator devices via attestation
Given the client submits a fresh mobile attestation token and integrity signals When the server validates the attestation and the verdict != PASS (e.g., rooted/jailbroken/emulator/debuggable detected) OR token is expired/invalid Then the server rejects the scan with error code DEVICE_INTEGRITY_FAILED And no attendance record is created And attestation result, device_id, os_version, and indicators are stored in the security log And the user is shown guidance to use a compliant device
Short-lived, signed, single-use scan tokens with replay protection
Given the client requests a scan token for reservation_id with authenticated account_id and device_id When the server issues a signed token containing {reservation_id, account_id, device_id, issued_at, nonce} with TTL <= 60s And the client presents the token with the scan Then the server verifies signature validity, freshness (now - issued_at <= TTL), allowed clock_skew, and nonce uniqueness And the token must be unused and bound to the same reservation_id, account_id, and device_id And on any failure (expired, reused, signature invalid, binding mismatch) the scan is rejected with TOKEN_INVALID or TOKEN_REPLAY And all token validation outcomes are logged
Bluetooth beacon enforcement when required
Given a venue has Require Beacon = true, a whitelist of beacon_ids, RSSI_threshold, and detection_window_s configured When the attendee scans Then at least one whitelisted beacon must be observed with RSSI >= RSSI_threshold within detection_window_s And if not satisfied, the scan is rejected with BEACON_NOT_PRESENT And when Require Beacon = false, detected beacons contribute to confidence_score but are not mandatory And beacon observations (ids, RSSI, timestamp) are stored with the attendance/security record
Server-side reservation and identity verification
Given the attendee is authenticated and has an active reservation for the class And device_id is registered to the account per device policy When a scan is received Then the server verifies reservation_id belongs to account_id, status is not already checked-in, and class capacity allows entry And mismatches or violations cause rejection with RESERVATION_MISMATCH, ALREADY_CHECKED_IN, or CAPACITY_REACHED as appropriate And successful scans link attendance to account_id, reservation_id, and device_id and update class rosters accordingly
Time-Windowed Check-in Control
"As an instructor, I want check-ins to be allowed only within a defined time window so that late or early arrivals are controlled and class flow and capacity remain predictable."
Description

Allow admins to configure an open/close time window relative to each class start/end (e.g., open 15 minutes before, close 10 minutes after) with late/early arrival policies and grace periods. The window is enforced alongside the geofence at scan time and is synchronized with ClassTap’s schedule (including time zones and daylight savings adjustments). Provide attendee-facing countdown and reasoned error messages, and allow staff overrides with role-based permissions and reason codes for audit. Ensure waitlist-to-payment flows respect the window to prevent last-minute remote conversions without presence.

Acceptance Criteria
On-time scan within configured window
Given a class start time of 10:00:00 in the venue’s timezone with check-in open offset = -15 minutes (opens 09:45:00) and close offset = +10 minutes (closes 10:10:00), and the attendee is within the configured geofence When the attendee scans at 09:45:00, 10:00:00, or 10:10:00 local venue time Then the scan is accepted and a single attendance record is created for the booking And the attendee-facing response displays “Checked in” with the venue-local timestamp And any additional scans for the same booking in this window are idempotent and do not create duplicates
Early scan blocked with live countdown
Given a class start time of 10:00:00 in the venue’s timezone with check-in open offset = -15 minutes (opens 09:45:00) When the attendee attempts to scan at 09:44:59 local venue time Then the scan is rejected with error code EARLY_CHECKIN and message including a countdown such as “Check-in opens in 00:00:01” And the countdown visibly updates at least once per second until the window opens And no attendance record is created and no financial transaction is triggered
Late scan within grace allowed and tagged
Given a class start time of 10:00:00 with check-in close offset = +10 minutes (closes 10:10:00) and a late grace period = 5 minutes When the attendee scans between 10:10:01 and 10:15:00 local venue time while within the geofence Then the scan is accepted and the attendance record is flagged Late And instructor/staff views display a Late badge for this check-in And the audit log records the exact scan timestamp and applied grace policy
Late scan beyond grace blocked with reasoned message
Given a class start time of 10:00:00 with close offset = +10 minutes and late grace period = 5 minutes When the attendee scans at or after 10:15:01 local venue time Then the scan is rejected with error code WINDOW_CLOSED and a message such as “Check-in closed 00:00:01 ago” And no attendance record is created And the failure is logged with the evaluated window bounds and attempt timestamp
Authorized staff override with reason codes and audit
Given a scan attempt that was rejected due to time window or geofence and a staff member with role-based permission OVERRIDE_CHECKIN When the staff initiates an override, selects a mandatory reason code from a configurable list, and confirms Then the attendee is checked in with an Override flag and the selected reason code attached to the attendance record And the audit log stores staff user ID, role, timestamp, original failure reason, override reason code, and location And analytics and exports distinguish overridden check-ins from standard check-ins
Window enforcement aligned to venue timezone and DST
Given a venue timezone (e.g., America/New_York) and a class scheduled on a DST transition day with start time defined in venue-local time, open offset = -15 minutes, close offset = +10 minutes When an attendee scans using a device set to any timezone or during the DST shift Then the system evaluates open/close boundaries strictly in the venue’s timezone using IANA TZ rules, handling non-existent and repeated local times correctly And acceptance/rejection matches the venue-local window boundaries regardless of device timezone And all displayed times and countdowns are shown in venue-local time with timezone abbreviation
Waitlist promotion requires presence in window
Given a waitlisted attendee receives a promotion offer within N minutes of class start and payment is auto-capture on acceptance When the attendee attempts to accept while outside the geofence or before the window opens or after it closes (including grace policy) Then the promotion is blocked, no payment is captured, and the user sees error code PRESENCE_REQUIRED with guidance to arrive onsite within the window When the attendee is inside the geofence and the window is open Then the promotion succeeds, payment is captured, and the check-in is recorded atomically in a single transaction
Multi-Zone Venue & Room Support
"As a studio coordinator, I want class check-ins restricted to the correct room zone so that attendees cannot check into the wrong class or overflow restricted spaces."
Description

Support multiple geofenced zones per venue to model separate entrances, rooms, or studios, including nested or adjacent areas. Classes can be assigned to a specific zone to prevent cross-room check-ins. Pop-up events can define temporary zones with start/end validity and per-event coordinates. The system validates scans against the assigned zone while rolling up attendance to the parent venue for reporting, enabling granular capacity control and compliance in multi-room or multi-entrance environments.

Acceptance Criteria
Class Assigned to Specific Zone
Given a venue has zones "Room A" and "Room B" with distinct geofences and an active class 10:00–11:00 assigned to "Room A" When a member scans between 09:55 and 11:05 inside the "Room A" geofence Then the scan is accepted and an attendance record is created with class_id, zone_id="Room A", and venue_id When a member scans inside "Room B" or outside any zone during the class window Then the scan is rejected with error_code="OUT_OF_ZONE" and no attendance is recorded When a member attempts to check in to the class from another zone Then cross-room check-in is prevented and a rejection reason is displayed
Nested Zone Validation Rules
Given a parent zone "Main Hall" and a nested child zone "Yoga Corner" fully contained within the parent geofence When a class is assigned to "Yoga Corner" and a scan occurs inside "Yoga Corner" Then the scan is accepted for that class and recorded with zone_id="Yoga Corner" When a scan occurs inside "Main Hall" but outside "Yoga Corner" for that class Then the scan is rejected with error_code="OUT_OF_ZONE" When a class is assigned to "Main Hall" and a scan occurs anywhere within the parent (including the child) Then the scan is accepted And all distance calculations use the configured zone geometry (polygon or radius) with a max location error tolerance configurable per venue
Adjacent Zones Boundary Determinism
Given two adjacent zones "Studio 1" and "Studio 2" with boundaries that meet or nearly overlap When a scan occurs within the shared boundary tolerance Then the system resolves the scan to the nearest zone by distance to the zone boundary (or centroid if equidistant) And if equidistant, the configured zone_priority determines the chosen zone And only the class assigned to the resolved zone can be checked in; no dual acceptance occurs And the decision is logged with resolved_zone_id, method (nearest|priority), and distance_meters
Temporary Pop-up Zone Activation Window
Given a pop-up event defines a temporary zone with coordinates and validity from 2025-09-10T17:00 to 2025-09-10T20:00 local time When a scan occurs within the zone geofence during the validity window for a class assigned to that pop-up zone Then the scan is accepted When a scan occurs outside the validity window or the zone is deactivated Then the scan is rejected with error_code="ZONE_INACTIVE" And the temporary zone is not selectable for classes outside its validity And the zone auto-expires and is hidden from selection after the end time
Attendance Roll-Up to Parent Venue Reporting
Given multiple classes run concurrently across zones within the same venue When attendance is recorded for each class in its assigned zone Then each attendance record includes venue_id and zone_id And venue-level reports aggregate attendance across all zones without double-counting And zone-level reports filter correctly to show only attendance from the selected zone And the sum of zone-level totals equals the venue-level total for the same time range
Zone-Level Capacity Enforcement
Given a zone has capacity 20 and a class assigned to that zone has class_capacity 30 When check-ins occur Then the effective capacity is min(zone_capacity, class_capacity)=20 and the 21st check-in is rejected with error_code="CAPACITY_FULL" And waitlist auto-promotions only occur when effective capacity is available in the assigned zone And capacity counts are isolated per zone; check-ins in other zones do not affect this class's capacity
Zone Administration and Validation
Given a venue admin creates or edits zones When saving a zone Then the system requires a unique zone name per venue, valid geometry (polygon or radius), and optional parent_zone_id for nesting And overlapping geometries are allowed only when a parent-child relationship is defined; otherwise the save is blocked with error_code="GEOMETRY_CONFLICT" And adjacency (touching boundaries) is allowed without error And deleting a zone is blocked if future classes reference it; a clear error is returned and dependencies are listed
Offline Check-in with Deferred Verification
"As a front-desk staff member, I want check-ins to work during connectivity outages so that classes can start smoothly and attendance is reconciled once we’re back online."
Description

Enable provisional offline check-ins when connectivity is poor by capturing the scan, local timestamp, last known location and accuracy, and policy version. Store the event securely on-device and sync when online for server-side validation against geofence and time-window rules. Apply a configurable grace period and flag exceptions that fail validation for admin review. Limit offline usage by time since last accurate fix and by number of provisional check-ins to mitigate abuse while preserving check-in continuity in low-connectivity venues.

Acceptance Criteria
Provisional Offline Check-in Capture
Given the device has no internet connectivity and an attendee's code is scanned for a scheduled class When the operator taps Check In Then the app creates a provisional check-in event with fields attendeeId, classId, localTimestamp, lastKnownLat, lastKnownLng, locationAccuracyMeters, lastFixTimestamp, policyVersion, deviceId, operatorId, checkinId (UUID), and offlineSequence And the event is written to encrypted on-device storage And the UI displays "Provisional check-in saved" within 1 second And no network request is attempted
Offline Usage Limits Enforcement
Given the device is offline and now - lastFixTimestamp > MaxOfflineLocationAgeMinutes When the operator attempts a check-in Then the app blocks the provisional check-in And the UI displays "Cannot check in: location too old" Given the device is offline and locationAccuracyMeters > MaxOfflineAccuracyMeters When the operator attempts a check-in Then the app blocks the provisional check-in And the UI displays "Cannot check in: location too inaccurate" Given the device is offline and provisional check-ins created today >= MaxProvisionalPerDevicePerDay When the operator attempts a check-in Then the app blocks the provisional check-in And the UI displays "Offline limit reached" Given the device is offline and all limits are satisfied When the operator attempts a check-in Then the app allows a provisional check-in
Deferred Server Validation and Confirmation
Given one or more provisional events are queued on-device When connectivity is restored or a manual Sync is triggered Then the client sends all queued events to the server with their captured fields And the server validates each against geofence (GeofenceRadiusMeters) and class time window ± GraceMinutes using the event's localTimestamp and lastKnownLocation And for each event that passes validation the server returns status Confirmed with serverReceivedAt And the client marks those events as Confirmed and updates the attendee's attendance in the class roster within 2 seconds Given an event fails geofence or time-window validation When the server responds Then the server returns status Exception with a reasonCode in {OutsideGeofence, OutsideTimeWindow, UnknownClass, UnknownAttendee} And the client marks the event as Flagged for review
Duplicate Prevention and Idempotency
Given an attendee already has a provisional or confirmed check-in for a class on the device When their code is scanned again while offline Then the app prevents creating a second provisional event for the same attendeeId+classId And the UI displays "Already checked in" Given the server receives a provisional event with a checkinId it has already processed When processing the payload Then the server returns 200 with status Unchanged and does not create a duplicate attendance record
Policy Version Handling
Given a provisional event includes policyVersion V that is recognized by the server When validating the event Then the server applies the geofence and time-window rules defined in V Given a provisional event includes policyVersion V that is not recognized When validating the event Then the server marks it as Exception with reasonCode UnknownPolicyVersion
Reliable Sync, Acknowledgement, and Retention
Given the client attempts to sync provisional events and the network fails or the server returns a 5xx error When retry policy is applied Then the client retries with exponential backoff up to RetryMaxAttempts and keeps events queued without loss or duplication Given the server has acknowledged a provisional event with Confirmed or Exception status When the client receives the acknowledgement Then the client marks the local event as synchronized and retains it for at least AuditRetentionDays And the encrypted payload cannot be edited after creation
Admin Audit Trail & Dispute Resolution
"As an administrator, I want a detailed audit trail and a way to resolve check-in disputes so that I can uphold policies, pay staff accurately, and pass compliance audits."
Description

Capture a tamper-evident audit record for each check-in including coordinates, accuracy, timestamp, device fingerprint hash, user ID, class ID, zone ID, and evaluated policy outcome. Provide an admin console to review events on a map, compare against policy, and resolve disputes with approve/deny actions and mandatory reason codes. Generate exports and reports for payroll, compliance, and insurer audits, and trigger alerts for anomalous patterns (e.g., repeated near-threshold check-ins or suspected spoofing) to help reduce abuse and support fair compensation.

Acceptance Criteria
Tamper‑Evident Audit Record on Check‑In
Given a check-in attempt is processed When the system evaluates the GeoFence Gate policy Then it writes exactly one append-only audit record containing latitude, longitude, horizontal_accuracy_m, timestamp_utc (ISO 8601), device_fingerprint_sha256, user_id, class_id, zone_id, policy_outcome (Allow|Deny), and policy_reason_code And the record includes hash_chain = SHA-256(previous_hash + record_payload) and is stored in a write-once medium And any attempt to update or delete the record is blocked and logged as a SecurityEvent with actor_user_id and timestamp_utc And the audit record is retrievable by event_id or request_id within 200 ms for the most recent 10,000 events
Admin Map Review and Policy Comparison
Given an admin with role = AuditViewer opens an audit event When the event detail page loads Then a map renders the event marker with an accuracy circle, the configured geofence boundary, and the venue location And the UI displays distance_m_to_center and within_radius = true|false And the UI displays within_time_window = true|false based on the configured start/end and the event timestamp_utc And filters by date_range, class_id, zone_id, and user_id return results within 2 seconds for up to 50,000 events
Dispute Resolution Workflow with Mandatory Reason Codes
Given an audit event is Deny or is flagged as contested When a user with role = DisputeResolver selects Resolve Then they must choose Approve or Deny and select a reason_code from a configurable list; if Other is selected, a free-text note (min 10 chars) is required And on Save, a resolution entry is created with resolver_user_id, action, reason_code, notes, timestamp_utc, and linked event_id; the original audit record remains unchanged And attendance status is updated accordingly and notifications are sent to instructor and attendee via in-app and email within 60 seconds And at most one active resolution exists per event; Reopen requires Admin role and creates a new resolution entry with supersedes_id
Anomaly Detection and Alerts for Near-Threshold or Spoofing
Given the stream of check-in audit events When a user_id or device_fingerprint_sha256 has ≥3 check-ins within 5 meters of the geofence boundary over any rolling 7-day window Then create a NearThresholdPattern alert within 60 seconds containing subject identifiers, counts, and sample event_ids And when mock-location is detected or location jumps >200 meters within 10 seconds while accuracy ≤ 30 meters Then create a SuspectedSpoofing alert within 60 seconds with supporting evidence fields And alerts are visible in the admin console and emailed to configured recipients; duplicate alerts for the same subject and pattern are suppressed for 1 hour
Payroll and Compliance Export Generation
Given an admin selects a date range and filters (class_id, zone_id, user_id) When Export is requested Then the system generates CSV and JSON files including per-event fields: event_id, user_id, class_id, zone_id, policy_outcome, policy_reason_code, timestamp_utc, latitude, longitude, horizontal_accuracy_m, device_fingerprint_sha256, distance_m_to_center, within_time_window, resolution_action, resolution_reason_code And exports up to 250,000 events complete within 60 seconds and are delivered via a signed URL that expires in 24 hours And CSV is UTF-8, RFC4180-compliant; timestamps are UTC ISO 8601; numeric fields use dot decimal
Role-Based Access Control and Admin Action Auditing
Given RBAC is configured When a user without AuditViewer or Admin role attempts to access the audit console or exports Then the system returns 403 Forbidden and logs the attempt with actor_user_id, endpoint, and timestamp_utc And any read of audit records and all dispute actions append an access_log entry that is hash-chained and immutable And device_fingerprint_sha256 is masked by default in the UI and can be revealed only by users with Admin role
Privacy, Consent & Data Minimization
"As a privacy-conscious attendee, I want to know why location is needed and have my data minimized and protected so that I feel safe using the app to check in."
Description

Display clear just-in-time notices explaining location use at check-in and obtain consent where required. Collect only point-in-time location needed for validation, with configurable precision and retention windows (e.g., 30–90 days) and optional coordinate rounding for stored records. Encrypt data in transit and at rest, segregate by region, and support subject access/deletion requests to meet GDPR/CCPA obligations. Provide org-level settings to balance enforcement strength with privacy, ensuring GeoFence Gate enhances trust while meeting legal and brand standards.

Acceptance Criteria
Just-in-Time Notice & Consent at Check-In
Given a member initiates a check-in scan for a booked class When GeoFence Gate requires location to validate attendance Then the system displays a just-in-time notice explaining why location is needed, the retention period, precision, and linking to the privacy policy before any OS permission prompt And if the member is in a jurisdiction requiring explicit consent, a distinct consent control is shown and must be affirmed to proceed And the system records consent status, timestamp, and notice version without storing location coordinates And if consent is declined or OS permission is denied, the geofenced auto-check-in is blocked and the org-configured fallback path is offered (e.g., manual verification) without collecting or storing location data
Point-in-Time Location Collection Only
Given the member has granted permission/consented and taps Confirm Check-In When the system captures location for GeoFence Gate validation Then exactly one point-in-time coordinate with timestamp and accuracy is captured per check-in attempt And no background or continuous location tracking occurs before or after the check-in flow And no raw OS location stream/history is persisted; only the single validated point is stored (subject to rounding configuration) And if multiple check-in attempts occur, each attempt creates at most one new location record linked to its attempt ID
Precision & Coordinate Rounding Configuration
Given an org admin selects a precision and rounding level (Exact, 50 m, 100 m, 250 m, 500 m) When a check-in location is stored Then the stored coordinates are rounded to the configured grid while validation distance uses the configured precision threshold And the just-in-time notice reflects the active precision/rounding setting And changes to precision/rounding apply to new check-ins only and do not reprocess historical records And an audit entry captures the admin, timestamp, old value, and new value for any precision/rounding change
Retention Window & Auto-Deletion
Given an org admin sets a location data retention window of 30, 60, or 90 days (default 60) When stored records exceed the configured retention Then they are automatically deleted from primary storage and replicas within 24 hours of exceeding the threshold And no location records older than the retention window are retrievable via UI or API And deletion jobs emit auditable logs with counts and identifiers of purged records And backup copies age out or are rendered irrecoverable within 30 days of the primary deletion, with an audit record of completion
Encryption in Transit and At Rest
Given any API or client pathway that transmits location or consent records When a connection is established Then TLS 1.2+ is enforced with HTTPS; non-TLS requests are rejected with 4xx and no data is accepted And location and consent data are encrypted at rest in databases and object storage using platform-managed encryption keys And encryption status is verifiable via automated checks in CI/CD or infrastructure monitoring
Regional Data Segregation & Residency
Given an org has selected a data region (e.g., EU, US) When location and consent data are written Then they are stored only in infrastructure within the selected region with no cross-region replication enabled And data processing for GeoFence Gate occurs within the selected region; cross-border transfers are blocked by policy and controls And region residency is verifiable via environment metadata (e.g., regional endpoints, storage bucket/DB region) and audit logs
Subject Access and Deletion Requests
Given a verified data subject or admin files a subject access request When the request is processed Then the system returns all location and consent records for that subject in a machine-readable format (CSV or JSON) within 30 days, including timestamps and venues but not higher precision than stored And given a verified deletion request, the system deletes the subject’s location records within 30 days, cascades deletions to indexes/caches, and returns a confirmation with an audit log entry And after deletion, attempting to retrieve the deleted subject’s location records via UI or API returns no results

Offline Seal

Enables reliable check‑in when Wi‑Fi or cellular is spotty. Attendee passes carry short‑lived, cryptographically signed tokens that the scanner can verify offline; results queue and auto‑sync with exact timestamps once back online. Keeps doors flowing at parks, basements, and busy lobbies.

Requirements

Short-Lived Pass Token Issuance & Signing
"As an organizer, I want attendee passes to carry signed tokens that expire and cannot be forged so that check-ins can be validated offline without risking fraud."
Description

Provision cryptographically signed, short‑lived tokens embedded in attendee passes (QR/NFC/wallet) containing minimal identifiers (event_id, pass_id), a validity window, a nonce/token_id, and a signature. Tokens are generated server-side using rotating signing keys and configurable TTL per event, designed to be verifiable entirely offline by scanners while minimizing exposure of PII. Support multiple encodings (static offline fallback token and rotating dynamic token when the attendee device is online), backward compatibility with existing pass formats, and safeguards for clock skew. Emit tokens at purchase and refresh at configurable intervals prior to doors; ensure one-time-use semantics are enforceable via token IDs upon scan and reconciliation.

Acceptance Criteria
Server-Side Token Issuance With Rotating Keys
Given an event has a configured token TTL and an active signing key When a pass is purchased or issued Then the server generates a token payload containing only event_id, pass_id, token_id (nonce), iat, nbf, and exp And the difference between exp and iat equals the event’s configured TTL And the token is signed using the currently active private key and includes the corresponding key_id And no PII (e.g., name, email, phone) appears in the payload And the token is serialized according to the pass format specification (QR/NFC/wallet)
Offline Signature and Validity Verification
Given a scanner is offline with a cached set of public keys and a configured clock skew window S When it scans a token whose signature matches the key referenced by key_id and the device time is within [nbf - S, exp + S] Then verification succeeds and the token is marked locally as eligible for redemption When the signature does not validate or the device time is outside the skew-adjusted window Then verification fails with a specific error reason: invalid_signature, not_yet_valid, or expired And if signing keys have rotated, tokens signed by previous keys still verify until their notAfter or token exp, whichever comes first
Dynamic Token Refresh Prior to Event
Given a dynamic refresh interval R is configured for the event and the attendee device is online When the current time enters the configured refresh window before the event Then the pass client automatically requests and receives a freshly signed token every R minutes And previously issued tokens remain valid until their exp And if a refresh attempt fails, the last valid token is retained and another attempt is made at the next interval And refreshed tokens are signed with the latest active key and include a new token_id
Static Offline Fallback Token
Given an attendee device is offline or does not support dynamic refresh When a pass is issued Then a static offline token is embedded with TTL matching the event’s offline fallback configuration and flagged as fallback=true And the payload includes token_id, nbf, exp and excludes PII And scanners can verify the token offline using cached keys And issuance is recorded with a fallback indicator for auditability
Multiple Encodings and Backward Compatibility
Given the platform supports QR, NFC, and wallet representations When a pass is generated Then the same token payload and signature are embedded across all selected encodings such that decoding yields byte-identical token bytes And legacy pass fields and layouts remain unchanged And legacy clients that do not understand the token field continue to render and scan passes without errors And feature flags or versioning can disable token embedding per tenant/event to preserve backward compatibility
One-Time-Use Enforcement and Reconciliation
Given each issued token has a unique token_id within the event scope When a token is first accepted by a scanner Then the scanner records token_id as consumed locally and returns status accepted_once And any subsequent scan of the same token_id on that device before sync is rejected with already_used And upon sync, the server reconciles token_ids across devices, marking duplicates as rejected and returning a reconciliation report And future scans of a reconciled (consumed) token_id are rejected server-side
Clock Skew Safeguards
Given scanners are configured with an allowed clock skew window S When a device clock differs from server time by up to ±S Then tokens with device time within [nbf - S, exp + S] verify successfully And tokens outside that range are rejected with not_yet_valid or expired And skew violations are recorded for monitoring and alerting
Offline Verification Engine
"As a door staff member, I want the scanner to verify passes without internet so that lines keep moving even when the venue has poor connectivity."
Description

On-device verification that parses tokens, validates digital signatures using cached public keys, enforces validity windows with tolerance for local clock drift, and returns definitive pass/fail results without network calls. The engine must handle high-throughput scanning with low latency, degrade gracefully if keys are stale, and surface actionable error codes. Results are written atomically to a local, durable store for subsequent sync. Implementation should be cross-platform for iOS/Android and packaged for reuse across branded apps.

Acceptance Criteria
Offline Pass Verification with Clock Drift Tolerance
Given the device has no network connectivity and cached public keys age <= 7 days And driftToleranceSeconds is configured to 120 (default) And a token signed by a cached key has a validity window that includes deviceTime within ±90 seconds When the token is scanned Then the engine performs zero network calls And returns outcome=PASS with errorCode=null and keyId present And verificationDurationMs <= 60 for p95 over 100 scans
Offline Rejection of Invalid Signature
Given the device is offline with a populated public-key cache And the token signature does not verify against any cached key (tampered or wrong key) When the token is scanned Then zero network calls are made And the engine returns outcome=FAIL with errorCode=SIGNATURE_INVALID and diagnostic reason is non-empty And verificationDurationMs <= 60
Validity Window Enforcement (Expired and Not-Yet-Valid)
Given driftToleranceSeconds=120 and the device is offline When a token with exp < deviceTime - 5 seconds is scanned Then outcome=FAIL and errorCode=TOKEN_EXPIRED When a token with nbf > deviceTime + 5 seconds is scanned Then outcome=FAIL and errorCode=TOKEN_NOT_YET_VALID When a token is scanned within ±driftToleranceSeconds of nbf/exp boundaries Then outcome=PASS and errorCode=null
Stale Public Key Graceful Degradation Policy
Given warnThresholdDays=7 and failThresholdDays=14 for key cache staleness When cached key age <= 7 days and a valid token is scanned Then outcome=PASS with no warnings When cached key age > 7 days and < 14 days and a valid token is scanned Then outcome=PASS and metadata.warnings includes KEY_CACHE_STALE_WARN When cached key age >= 14 days and any token is scanned Then outcome=FAIL with errorCode=KEY_CACHE_STALE_HARD_FAIL And in all cases zero network calls are made
High-Throughput, Low-Latency Verification Performance
Given the device is offline with a warmed key cache and a shuffled set of 1,200 tokens (70% valid, 30% invalid/expired) When scanning at a sustained 10 scans/second for 3 minutes on reference devices (iPhone 12, Pixel 5 or newer) Then verificationDurationMs p95 <= 60 and p99 <= 100 And dropRate=0 (no missed scans) and processingQueueDepth never exceeds 3 And median CPU utilization <= 35% and peak RSS increase <= 64 MB during the run
Cross-Platform Engine Parity and Packaged Reuse
Given a canonical test suite of 500 tokens covering valid, invalid, expired, not-yet-valid, and stale-key cases with clock offsets {-180,-120,0,120,180} When executed against the iOS and Android engine packages of the same semantic version Then for every test, outcome and errorCode are identical across platforms And the public API surface (method names, parameters, error code enum) matches the shared spec And per-platform binary size overhead added by the engine <= 3 MB And 100% of the suite passes on both platforms
Atomic, Durable Local Write of Results
Given offline scans produce N verification results and each write uses a single logical transaction When the app crashes, is OS-killed, or power is lost between writes Then upon restart, each result is either fully present exactly once or absent (no partial rows, no duplicates) And persisted fields include tokenId, outcome, errorCode (nullable), scanTimestampUtc, verificationDurationMs, keyId, deviceId, attemptId And concurrent scans (>=4 worker threads) maintain consistency with no lost or duplicated entries And writeDurationMs average <= 10 and p95 <= 20
Local Check-in Queue & Auto-Sync
"As an organizer, I want offline scans to queue and auto-sync with exact timestamps so that attendance records stay accurate once we’re back online."
Description

Durable local queue that records each scan outcome (accepted, rejected, override) with precise device timestamps, token_id, pass_id, operator_id, and device_id. When connectivity resumes, the client batches and transmits records in order with idempotency keys, receives server acknowledgements, and reconciles conflicts (e.g., duplicate scans across devices) to a single canonical attendance record. Includes retry with exponential backoff, back-pressure limits to protect device storage, and visibility into queue length and sync status. Server APIs must be idempotent and return authoritative check-in state per pass.

Acceptance Criteria
Offline Scan Recorded with Required Metadata and Durability
Given the device has no internet connectivity When an operator scans an attendee pass Then a queue record is created with outcome in {accepted, rejected, override}, device_timestamp at millisecond precision in RFC3339, token_id, pass_id, operator_id, and device_id Given a queue record is created When the app is force-closed or the device reboots before connectivity returns Then the record persists on disk and is available on next app launch Given an operator selects override during scan When the record is queued Then outcome equals override and operator_id is non-null
FIFO Batching and Ordered Transmission on Connectivity Resume
Given queued scan records exist and connectivity resumes When the client begins syncing Then records are transmitted in the exact FIFO order they were queued and grouped into batches not exceeding the configured batch_size Given a batch contains N records When the server acknowledges records 1..k and returns transient errors for k+1..N Then records 1..k are removed from the queue and records k+1..N are retained and retried preserving their original order Given sync is in progress When connectivity drops again Then syncing pauses without data loss and resumes automatically when connectivity returns
Idempotent Server Processing and Acknowledgements
Given the client posts a scan record with idempotency_key X When the same record is posted again with idempotency_key X Then the server returns HTTP 200 and the same server_record_id and check-in result without creating duplicates Given the server processes a new scan record When it responds Then the response includes server_record_id, authoritative_state per pass (e.g., present/absent/duplicate), and server_received_at timestamp Given the client receives an acknowledgement When updating local storage Then the local record is marked as synced with the returned server_record_id and authoritative_state
Cross-Device Duplicate Scan Reconciliation to Canonical Attendance
Given the same pass is scanned on two devices within the event window When both devices sync their queues Then the server reconciles to a single canonical attendance record for that pass and returns authoritative_state to both clients Given a local record’s outcome conflicts with the authoritative_state When the client processes the acknowledgement Then the local record is updated to reflect the authoritative_state and flagged as reconciled Given multiple duplicate scans exist for a pass When reconciliation completes Then no more than one 'present' attendance entry exists server-side for that pass and event
Retry with Exponential Backoff and Resume
Given a sync attempt fails due to network error or HTTP 5xx When the client retries Then retries use exponential backoff with jitter and continue until an acknowledgement is received or the app is terminated Given the app is terminated during backoff When the app is relaunched Then the retry schedule resumes from persisted state without resending already acknowledged records Given the failure is a client error (HTTP 4xx excluding 429) When evaluating retry Then the record is marked as failed with an error reason and is not retried automatically
Back-Pressure and Storage Protection
Given the local queue size approaches the configured storage threshold When new scans are attempted Then the app warns the operator with a visible message showing remaining capacity Given the storage threshold is reached When an additional scan is attempted Then the app blocks further scans, records no new data, and instructs the operator to restore connectivity to sync before proceeding Given back-pressure is active When connectivity is restored and records sync below the threshold Then scanning is automatically unblocked
Operator Visibility into Queue Length and Sync Status
Given the device is offline or syncing When the operator opens the check-in screen Then the UI displays queue_length, last_successful_sync timestamp, and current sync_status (e.g., paused/offline/syncing/failed) without requiring connectivity Given the queue_length or sync_status changes When observing the UI Then the displayed values update within 1 second of the change Given a record fails to sync When viewing the queue details Then the operator can see which records failed and the last error reason
Anti-Replay & Duplicate Prevention
"As a door staff member, I want the system to block duplicate or replayed scans so that each pass admits only its allowed entries."
Description

Mechanisms to prevent token replay and duplicate admissions while offline. The scanner maintains a local ledger of seen token_ids and pass_ids with bounded retention, rejects re-scans according to pass rules (single-entry vs multi-entry), and captures suspicious attempts for audit. Upon sync, the server consolidates multi-device duplicates using first-seen timestamp policy and updates pass state; the client receives resolution outcomes for transparency. Configurable policies define whether overrides are allowed and how many re-entries are permitted.

Acceptance Criteria
Single-Entry Pass Rescan on Same Device (Offline)
Given the scanner device is offline And a valid, unused single-entry pass is scanned When the pass is scanned the first time Then the scan is accepted and an admission event is queued with fields {pass_id, token_id, first_seen_ts, device_id, status="admitted"} And the pair {pass_id, token_id} is recorded in the local ledger When the same pass is rescanned while offline within the token TTL Then the scan is rejected with reason "duplicate_single_entry" And no additional admission event is queued And a suspicious_attempt record is stored and its count is incremented for this pass_id
Concurrent Scans on Multiple Devices Resolved by First-Seen (Sync)
Given two scanner devices A and B are offline and their clocks are within ±2 seconds of the last server sync And the same single-entry pass is scanned on device A and device B within 60 seconds When both devices reconnect and sync Then the server accepts only the scan with the earliest first_seen_ts as "admitted" And marks the other scan(s) as "rejected_server_duplicate" And updates the pass state to "used" And returns resolution outcomes to both devices And each device updates its local ledger to reflect the server outcome within 2 seconds of receipt
Multi-Entry Pass With Configured Re-Entry Limit (Offline)
Given a pass with re_entry_limit=3 and re_entry_window=24h and the scanner is offline When the pass is scanned the 1st, 2nd, and 3rd time within the window Then each scan is accepted and queued with sequence numbers 1, 2, 3 and distinct token_ids When the pass is scanned a 4th time within the window while still offline Then the scan is rejected with reason "re_entry_limit_exceeded" And a suspicious_attempt record is stored When connectivity is restored and sync occurs Then the server consolidates all queued admissions across devices by first_seen_ts and enforces the re_entry_limit And any admissions beyond the limit are marked "rejected_server_limit" and returned to clients
Short-Lived Token Expiration and Clock Skew Handling
Given token_TTL=120 seconds and allowed_clock_skew=30 seconds and the device clock is within the allowed skew When a pass token is scanned after token_iat + token_TTL + allowed_clock_skew Then the scan is rejected with reason "token_expired" And no admission is queued When a pass token is scanned at or before token_iat + token_TTL + allowed_clock_skew Then the token signature is verified offline And processing continues according to pass rules
Local Ledger Bounded Retention and Eviction
Given the local ledger has a capacity of 50,000 entries or 14 days of history, whichever is reached first When the capacity or retention limit is reached Then the system evicts oldest entries with resolved_status first, retaining entries with statuses in {pending_sync, awaiting_server_resolution} And entries are never evicted before 21 days if they are unresolved And eviction preserves the ability to detect duplicates for remaining entries And an eviction summary with count and last_evicted_ts is logged locally and queued for sync
Suspicious Attempt Capture and Audit Trail
Given a scan is rejected offline for reason in {duplicate_single_entry, re_entry_limit_exceeded, signature_invalid, token_expired, replay_token_id_seen} When the rejection occurs Then a suspicious_attempt record is stored with fields {pass_id, token_id, reason, device_id, first_seen_ts} And attempts for the same pass_id within 10 minutes increment attempts_count When connectivity is restored and sync occurs Then suspicious_attempt records are uploaded and made available via the audit API within 60 seconds And local records are marked as synced
Operator Override Policy Enforcement
Given overrides_allowed=false for the event and the device is offline When an operator attempts to override a duplicate rejection Then the override is blocked with error "Overrides disabled by policy" And no admission event is queued Given overrides_allowed=true and max_offline_overrides=2 When the operator performs an override on a duplicate scan Then an "admitted_override" event is queued including fields {operator_id, reason, pass_id, token_id} And a visual banner indicates "Override used (1/2)" When the operator attempts a 3rd override Then the action is blocked with error "Override limit reached" When connectivity is restored and sync occurs Then the server validates and applies overrides only if pass capacity exists; otherwise marks them "rejected_server_limit" and returns outcomes to the client
Key Distribution, Rotation & Revocation
"As a system admin, I want signing keys to be securely distributed and rotated to devices so that tokens remain verifiable even if keys change or are revoked."
Description

Secure distribution and caching of verification key material to scanning devices. The app periodically fetches signed key bundles, supports overlapping key rotation windows, honors key expiry, and can revoke compromised keys immediately upon reconnect. Key bundles are stored encrypted at rest, tied to organization/event scope, and exposed via a versioned interface to the verification engine. The system surfaces last-updated times, warns when keys are nearing expiry, and blocks scans if key trust cannot be established according to policy.

Acceptance Criteria
Periodic Fetch and Integrity Verification of Key Bundles
Given the device has at least one previously cached key bundle and network connectivity is available When the configured fetch interval elapses or a manual refresh is initiated Then the app requests the latest key bundle for the active organization/event scope And verifies the bundle’s signature using the pinned publisher public key And confirms the current time is within the bundle’s not_before and not_after window And rejects and does not cache the bundle if signature or validity checks fail And, upon successful validation, atomically replaces the cached bundle for that scope And updates the surfaced last-updated timestamp to the server-provided bundle timestamp
Encrypted-at-Rest Storage Scoped to Org/Event
Given a validated key bundle is to be stored on device When persisting the bundle Then the raw key material is encrypted at rest using the device keystore with a non-exportable key And the bundle is stored namespaced by organization_id and event_id And attempts to access keys outside the active scope return not found And no unencrypted key material is written to disk or logs And a device compromise test that reads the storage file reveals only ciphertext
Overlapping Key Rotation Window Handling
Given the cached bundle contains overlapping active keys (current_key and next_key) for the same scope When verifying an attendee token during the overlap window Then the verification engine accepts tokens signed by either key if the token timestamp is within the key’s validity window And upon expiry of current_key, it is immediately removed from the active set without requiring a restart or manual action And newly fetched bundles that introduce a future key are added without disrupting verification of currently valid keys
Immediate Revocation on Reconnect
Given one or more keys for a scope have been revoked server-side while the device was offline When the device regains connectivity Then the app fetches the latest bundle/revocation list before processing any new scans And removes revoked keys from the active set and cache within the current reconnect session And blocks further scanning for that scope until revocation has been applied successfully And records an audit event with key identifiers and timestamps for the revocation application
Expiry Warning and Scan Blocking Policy
Given the active keys for a scope are approaching expiry per policy threshold When the remaining validity for the last usable key is below the threshold Then the app surfaces a warning in the scanner UI and emits a telemetry event with time-to-expiry And if all keys in the scope are expired or trust cannot be established per policy, scanning is blocked with a clear error state and code And queued offline results are preserved but not submitted until valid keys are present
Versioned Interface Exposure to Verification Engine
Given the verification engine requests key material via the versioned interface When a bundle is provided with interface_version and bundle_version metadata Then bundles with a matching major interface version are accepted and parsed And bundles with a higher major interface version are rejected with an “upgrade required” error surfaced to the UI and logs And the engine selects the highest supported minor version behavior when multiple are present And version and parsing outcomes are included in diagnostic logs
Last-Updated Timestamp Surfacing
Given a key bundle is validated and cached for a scope When viewing key status in the app or via diagnostics API Then the last-updated timestamp and source (server time) are displayed in RFC 3339 format and local relative time And the timestamp updates only after successful validation and atomic write And if no valid bundle exists, the UI shows “Never” and scanning is blocked per policy
Operator Offline UX & Safeguards
"As a door staff member, I want clear offline indicators and controls so that I can operate confidently and handle exceptions while the network is down."
Description

Operator-facing offline mode with clear status indicators, large visual affordances for pass/fail, audible/haptic feedback, and real-time counters for queued check-ins. Includes manual override with staff PIN and required reason codes, fallback manual lookup by attendee name when tokens cannot be read, and controls to enable/disable offline acceptance by event. UI must remain performant in low light and crowded conditions, support accessibility standards, and guide staff during conflict resolution after sync.

Acceptance Criteria
Offline Mode Status & Indicators
Given the device loses internet connectivity during an active event When the app detects network unavailability Then it enters Offline state within 1 second, displays a persistent "Offline" banner and icon, disables network-dependent actions, and enables offline scanning Given connectivity is restored When the app reconnects Then it displays "Reconnecting…" then "Syncing N check-ins…", attempts sync, and removes the offline banner only after all queued items have attempted sync Given a screen reader is active When the connectivity state changes Then an accessibility announcement describes the new state and queued count Rule: The offline/online state and queued count are visible at all times on the scanner screen
Offline Pass Scan Success & Double-Scan Prevention
Given a valid, unredeemed, cryptographically signed token is scanned while offline When the scan is processed Then the UI shows a full-screen green confirmation with attendee name and event title, meets WCAG AA contrast (>=4.5:1), vibrates once (50–80 ms), plays a short high-tone beep (<=500 ms), records a local check-in timestamp to millisecond precision, increments the queued counter by 1, and rate-limits further scans for 800 ms Given the same token is scanned again while still offline for the same event When processed Then the app flags as duplicate with a red banner, vibrates twice, plays a low-tone buzz, does not increment the queue, and logs the duplicate attempt locally Performance rule: Median offline token validation time <=300 ms and 95th percentile <=500 ms on supported devices
Offline Scan Invalid/Expired Handling
Given an expired, revoked, or signature-invalid token is scanned while offline When processed Then the UI displays a full-screen red failure state meeting WCAG AA contrast (>=4.5:1) with a specific reason label ("Expired", "Revoked", or "Invalid signature"), provides clear next actions ("Manual override" and "Manual lookup"), vibrates twice, plays a low-tone buzz, and does not create a queued check-in Rule: An audit entry is stored locally for each failed scan with reason, token fingerprint, timestamp, and device ID
Queued Check-ins Counter, Auto-Sync, and Conflict Resolution
Given N offline check-ins are queued When viewing the scanner screen Then a visible counter displays N, and tapping it opens a list showing attendee name, token fingerprint suffix, local timestamp, and status (Queued/Sending/Failed/Synced) Given connectivity is restored When auto-sync starts Then all queued items begin syncing within 5 seconds; successful items are marked Synced and retain their original local timestamp as the official check-in time in the backend Given a conflict occurs during sync (e.g., token already redeemed online, token revoked before event start, capacity exceeded) When conflicts are detected Then the app presents a guided resolution view per item with recommended action and options Accept/Reject; staff must choose a reason, decisions are audit-logged with user ID and device ID, and the queue updates accordingly Rule: Failed sync attempts are retried up to 3 times with exponential backoff; after 3 failures, items are marked Failed with a visible badge and can be retried manually
Manual Override with Staff PIN and Reason Codes
Given a staff member taps "Manual override" from a failed or unreadable scan When prompted for credentials Then the app requires a 6-digit staff PIN verified offline against a locally cached encrypted list; after successful entry, the staff must select a reason code from a configurable list before saving Given an override is confirmed When it is queued Then the queued item is tagged as Override with staff ID, reason code, timestamp, and attendee reference, and is included in the next sync; override entries are clearly labeled in the queue and audit logs Security rule: After 3 consecutive incorrect PIN attempts, override is disabled for 5 minutes and attempts are logged; if event policy disables overrides, the action is blocked with an explanatory message
Fallback Manual Lookup by Attendee Name
Given a token cannot be read or an attendee lacks a token When the staff selects Manual lookup Then an offline roster (cached pre-event) is searchable by name/email with partial matching; results return within 500 ms and display up to 20 matches with identifying details Given an attendee is selected from results When staff confirms check-in Then an offline check-in is queued with attendee ID, method set to Name Lookup, timestamp recorded, and duplicate prevention enforced using cached state; if capacity/eligibility data is stale, a warning is shown and an Override flow is offered
Per-Event Offline Acceptance Controls & Enforcement
Given an event has Allow offline check-ins disabled When the device is offline and a token is scanned Then the app performs validation-only (no queuing), displays "Validation only: not recorded" messaging, blocks Manual override, and allows Manual lookup in view-only mode Given an event has Allow offline check-ins enabled When the device is offline Then scanning and manual lookup can create queued entries subject to duplicate and policy checks Given a staff member with Manager role edits event settings When toggling Allow offline check-ins Then a confirmation with risk explanation is shown, the change is logged with user ID and timestamp, and the setting takes effect on next sync to devices

SafeSwap Reissue

Self‑service device change with guardrails. Clients verify via email/SMS and recent‑activity checks; old passes are revoked instantly and fully audited. Optional staff approval for high‑value events. Minimizes support while preserving strong protection against pass sharing.

Requirements

Self-Service SafeSwap UI Flow
"As a client, I want a clear self-service way to move my pass to a new device so that I can keep booking classes without waiting for support."
Description

Provide a guided, mobile-first flow within My Passes that lets clients initiate a device change, understand the steps (verification, possible review, confirmation), and complete the process without staff intervention. The flow includes entry points from pass details, booking checkout, and help links; clear state messaging (pending verification, under review, completed); real-time validation; and immediate display of the reissued pass/QR upon success. It supports localization, accessibility (WCAG AA), and brand theming. It integrates with existing booking and attendance modules so the new device can check in and book immediately while preventing concurrent use from the old device. It exposes error handling for edge cases (expired codes, network timeouts) and offers safe retry without duplicating requests.

Acceptance Criteria
Entry Points: Pass Details, Checkout, and Help Links
- Given a signed-in client with at least one active pass, When they open Pass Details on mobile, Then a "Change device (SafeSwap)" CTA is visible and enabled. - Given a client at booking checkout with a pass blocked due to device mismatch, When they select "Use this pass", Then a contextual prompt offers SafeSwap and deep-links into the flow. - Given a client viewing Help > Pass access issues, When they tap "Change device", Then the flow opens on Step 1 with context preserved (origin=help). - Then all entry points emit analytics event safeSwap_entry with source and pass_id.
Guided Mobile-First SafeSwap Stepper
- Given the SafeSwap flow is opened on a mobile viewport 320–428 px wide, When the screen loads, Then a 3-step progress indicator displays the step titles: Verify identity, Review (optional), Confirm. - Then primary and secondary CTAs are visible above the fold on a 667 px height device. - When the user navigates back or closes, Then progress is persisted and restored if the flow is reopened within 24 hours. - When the network is offline, Then the primary CTA is disabled and an inline offline message is shown. - When a request is in progress, Then the primary CTA shows a spinner and is debounced (max 1 submission per 1.5 seconds).
Verification via Email/SMS with Real-Time Validation
- Given the client has a verified email and phone on file, When Step 1 renders, Then both delivery options are shown with masked values and the last 2 digits/characters visible. - When the user selects a method and requests a code, Then a 6-digit code is issued with a TTL of 10 minutes and a resend cooldown of 30 seconds; max 5 sends per hour per user per method. - Then real-time validation enforces 6 numeric digits; paste is allowed; SMS auto-fill is supported where available. - When the correct code is entered, Then verification succeeds within 2 seconds and a risk check is executed. - When risk=low, Then the flow proceeds to confirmation; When risk=medium/high or the booking value >= configured threshold, Then route to Under Review.
Edge Cases: Expired/Invalid Codes and Network Timeouts with Safe Retry
- When a code is expired, Then the UI shows "Code expired" and offers Resend; resends respect cooldowns and rate limits. - When a code is invalid 3 times, Then show "Too many attempts" and lock code entry for 5 minutes; offer "Try a different method". - When verification or reissue requests time out (>8s) or return 5xx, Then show a retry banner and allow retry; retries are idempotent using a client-generated idempotency key. - Given idempotency key k is reused after a timeout, When the original request eventually succeeds, Then the client resolves the flow once without duplicate reissues, notifications, or audit entries.
Optional Staff Approval and Under-Review State
- Given risk=medium/high or event value >= configured threshold T, When the user completes verification, Then the flow shows Under Review with an ETA of <=24 hours and clear next steps. - Then staff receive a review task containing pass_id, user_id, device_fingerprint, geo (city/region), and recent activity summary. - When staff approve, Then the client is notified (email/SMS/push) and the flow advances to confirmation without extra input. - When staff deny, Then the flow displays a denial explanation and a contact support link; no reissue occurs and old device stays authorized. - Then all transitions (submitted, approved/denied) are recorded with timestamp, actor, and reason in the audit log.
Immediate Reissue, Old Device Revocation, and Concurrency Prevention
- When a low-risk verification completes, Then the pass is reissued to the new device within 5 seconds and the confirmation screen displays the new QR immediately. - Then the old device's pass sessions/tokens are revoked within 5 seconds; attempts to check in or book from the old device are blocked with error "Pass reissued to a new device". - Given the new device, When the user attempts to check in or book immediately after confirmation, Then the request succeeds on the first attempt. - Then concurrency is prevented: at most one active device can use the pass; simultaneous requests from old and new devices result in the old being rejected. - Then an audit record is created including old_device_id, new_device_id, verification method, risk score, IPs, and revocation timestamp.
Localization, Accessibility (WCAG AA), and Brand Theming Compliance
- Given locale fr-FR or es-ES, When the flow renders, Then all strings are localized and fit at 320 px width without truncation; long text wraps gracefully. - When system font size is set to 200%, Then all content remains readable and operable; interactive targets are >=44x44 pt. - Then text and UI contrast meet WCAG AA; focus order follows visual order; all inputs/CTAs have accessible names; status messages use aria-live and are announced. - Then the flow is operable via keyboard and screen reader; error messages are reachable and announced when they appear. - When a brand theme is applied, Then only design tokens are used; contrast remains compliant; logos render sharply on 2x/3x DPI.
Dual-Channel Identity Verification (Email/SMS OTP)
"As a security-conscious client, I want to verify my identity via email or text so that only I can move my pass to a new device."
Description

Implement OTP verification using both email and SMS against the account’s verified contacts, with configurable code length, expiry, resend throttling, and maximum attempts. The user must pass at least one channel, with optional policy to require both for higher-risk cases. Messages use brandable templates and regional sender settings. The service records verification events, binds them to the SafeSwap session, and gates progression to reissue. Integrates with existing comms providers (e.g., Twilio/SendGrid) via abstraction to allow provider failover and includes detailed delivery/receipt telemetry for support diagnostics.

Acceptance Criteria
Email OTP Verification with Configurable Code Length and Expiry
Given a SafeSwap reissue session with a verified email and policy code_length=L, expiry_seconds=E, and max_attempts=A When the user requests an email OTP Then an OTP of length L is generated and scoped to the session with TTL E and single-use And any previously issued email OTP for this session is invalidated And the OTP is sent via the configured primary email provider using the active brand template and regional sender settings And a VerificationRequested event is recorded with session_id, channel=email, provider_message_id, and timestamp Given the user submits the correct email OTP before expiry and within A attempts When the system validates the OTP Then verification succeeds, a VerificationSucceeded event is recorded, and the session marks the email channel as verified Given the user submits an incorrect or expired email OTP or exceeds A attempts When validation occurs Then verification fails, the channel is locked per policy (session-only or cooldown), and a VerificationFailed event is recorded with reason
SMS OTP Verification with Configurable Code Length and Expiry
Given a SafeSwap reissue session with a verified phone and policy code_length=L, expiry_seconds=E, and max_attempts=A When the user requests an SMS OTP Then an OTP of length L is generated and scoped to the session with TTL E and single-use And any previously issued SMS OTP for this session is invalidated And the OTP is sent via the configured primary SMS provider using the active brand template and regional sender settings And a VerificationRequested event is recorded with session_id, channel=sms, provider_message_id, and timestamp Given the user submits the correct SMS OTP before expiry and within A attempts When the system validates the OTP Then verification succeeds, a VerificationSucceeded event is recorded, and the session marks the SMS channel as verified Given the user submits an incorrect or expired SMS OTP or exceeds A attempts When validation occurs Then verification fails, the channel is locked per policy (session-only or cooldown), and a VerificationFailed event is recorded with reason
Policy Enforcement: At-Least-One vs Require-Both Channels
Given policy requirement=at_least_one When either email or SMS channel is verified successfully Then the session is marked verification_satisfied and the user can proceed to SafeSwap reissue Given policy requirement=require_both When only one channel is verified Then verification is not satisfied and the system prompts the user to verify the remaining channel Given policy requirement=require_both When both channels are verified successfully Then the session is marked verification_satisfied and the user can proceed to SafeSwap reissue
Resend Throttling and Max Attempt Lockout with Cooldowns
Given resend_throttle_seconds=R and max_resends_per_channel=MR for the session When the user requests a resend faster than R or beyond MR Then the request is rejected, no OTP is generated or sent, and a Throttled event is recorded with reason Given max_attempts=A and cooldown_seconds=C after lockout When the user reaches A validation attempts for a channel Then further attempts are blocked per policy (for C seconds or for the session), and a ChannelLocked event is recorded Given a resend is honored for a channel When a new OTP is generated Then all prior OTPs for that channel and session are invalidated immediately
Provider Failover and Delivery/Receipt Telemetry
Given a configured primary and secondary provider for a channel and a delivery_receipt_timeout=T When sending via the primary provider fails with a retriable error or delivery receipt is not received within T Then the system retries once with the secondary provider, deduplicates the message, and records ProviderFailover telemetry tied to the session and channel Given any send attempt to a provider When the provider returns message identifiers and callbacks Then provider_name, message_id, delivery_status, error_code (if any), and latency_ms are recorded and linked to the session_id and channel Given support opens diagnostics for a session When telemetry exists Then the last N send attempts per channel are displayed with provider, status, timestamps, and reasons
Brandable Templates and Regional Sender Selection
Given an active brand template and regional sender settings for the user's locale When an OTP is sent via email or SMS Then the message content uses the brand template variables (code, expiry, organization name) and the sender ID/short code aligns with the region configuration Given a region with restricted sender types When an incompatible sender is configured Then the send is blocked, a Misconfiguration event is recorded, and an actionable error is surfaced to admins Given multiple brands are configured for the account When a SafeSwap session is initiated under brand B Then all OTP messages for that session use brand B's templates and assets
Audit Logging and SafeSwap Session Gate
Given any verification event (requested, succeeded, failed, throttled, locked, provider_failover) When the event occurs Then an audit record is written with session_id, account_id, user_id (if available), channel, ip, user_agent, policy_snapshot, timestamp, and outcome Given a session has not met verification_satisfied per policy When the user attempts to proceed to reissue Then access is denied with a verification_required response and an audit record is created Given audit records exist for a session When staff retrieves the audit trail Then records are immutable, filterable by session_id, and sensitive fields are masked per privacy settings
Recent-Activity Risk Evaluation
"As a platform, I want to automatically assess the risk of a device change so that low-risk requests are approved quickly and high-risk ones get extra scrutiny."
Description

Calculate a risk score for each SafeSwap request using recent signals: last successful login, last check-in location and time, device fingerprint change, IP reputation and geodistance, payment method changes, account age, and number of recent swaps. Based on configurable thresholds, auto-approve low-risk requests, challenge with dual verification for medium risk, or route to staff approval for high-risk/high-value passes or events. Provide an admin settings page for signal weights and thresholds, with defaults aligned to platform policy. Emit structured risk decisions for analytics and auditing.

Acceptance Criteria
Risk Score Calculation from Recent Signals
Given a SafeSwap request with recent-activity signals (last_successful_login, last_checkin_time/location, device_fingerprint_change, ip_reputation, geo_distance, payment_method_change, account_age_days, recent_swaps_30d) And a risk configuration defining per-signal weights and 0–100 normalization When the risk evaluator runs Then it computes a risk_score within [0,100] And includes per-signal normalized values, weights, and contributions in the decision payload And applies the configured missing_signal_penalty to unavailable signals and flags them And persists the raw signals snapshot, computed score, thresholds used, and evaluator_version with a timestamp
Auto-Approve Path for Low Risk
Given thresholds.dual_verify_min and thresholds.staff_review_min are configured And a SafeSwap request evaluates to risk_score < thresholds.dual_verify_min When the evaluation completes Then decision.action = "auto_approve" And no dual verification challenge is triggered And the decision payload includes {risk_score, risk_level: "low", thresholds, action} And the SafeSwap proceeds without staff intervention
Dual Verification Challenge for Medium Risk
Given thresholds.dual_verify_min <= risk_score < thresholds.staff_review_min And the account has both a verified email and a verified phone number When the evaluation completes Then decision.action = "dual_verify" And one-time codes are sent to both channels (email and SMS) And the SafeSwap is approved only after both codes are successfully verified within 10 minutes And failure or timeout on either channel escalates the decision to "staff_review" and is recorded in the audit log And if either channel is unavailable or unverified, decision.action = "staff_review"
Staff Approval Routing for High Risk or High-Value
Given risk_score >= thresholds.staff_review_min When the evaluation completes Then decision.action = "staff_review" And the request is queued for review with priority proportional to risk_score And the decision payload includes top_contributing_signals Given config.high_value_staff_required = true and (event.high_value = true or event.price >= config.high_value_min_amount) When the evaluation completes (for any risk_score) Then decision.action = "staff_review" And the decision payload includes high_value = true and triggering criteria
Admin Config: Weights, Thresholds, and Validation
Given an admin with permissions opens Risk Settings When the page loads Then it displays editable weights for each signal, thresholds.dual_verify_min, thresholds.staff_review_min, a high_value_staff_required toggle, and high_value_min_amount And default values are pre-populated from the platform default profile When the admin attempts to save invalid values (weights outside [0,100]; thresholds outside [0,100]; dual_verify_min >= staff_review_min; high_value_min_amount < 0) Then the form blocks save and shows inline validation errors per field When the admin saves valid values Then the configuration persists with a new version, is auditable (who/when/what changed), and is applied to subsequent evaluations And a Reset to defaults action restores the platform default profile values
Structured Risk Decision Emission and Audit Logging
Given any SafeSwap risk evaluation completes When a decision is produced (auto_approve, dual_verify, or staff_review) Then a risk_decision event is emitted to the analytics stream with fields: request_id, user_id, event_id, risk_score, risk_level, action, thresholds, signal_contributions[], evaluator_version, timestamp, correlation_id And an immutable audit record is stored with the same payload plus requester_ip and reviewer_id (if staff_review) And both records are retrievable by request_id within 2 seconds of decision time
Instant Pass Revocation & Token Rebind
"As a client, I want my old device access revoked instantly when I swap so that my pass can’t be used by someone else."
Description

Upon successful verification/approval, immediately revoke the old device’s access by invalidating sessions, cached QR/barcodes, API tokens, and webhooks tied to the pass. Rebind the pass to the new device with fresh credentials and rotate identifiers to prevent replay. Ensure check-in scanners perform server-side validation to reject revoked codes, including offline grace policies that minimize fraud windows. Provide idempotent operations, transactional integrity across subsystems (booking, attendance, invoicing), and user feedback confirming the old device is deactivated. Propagate revocation events to integrations and logs.

Acceptance Criteria
Immediate Session and Token Revocation on SafeSwap Approval
Given a pass bound to an old device with active app/web sessions, QR/barcode, API tokens, and pass-bound secrets And the client has completed SafeSwap verification and any required staff approval When SafeSwap is approved Then all app/web sessions for the old device are invalidated within 5 seconds And server validation of the old QR/barcode returns a revoked response And API calls using old tokens are rejected with HTTP 401 within 5 seconds And any pass-bound secrets tied to the old device are rotated and the old values no longer validate And an audit entry records the revocation with timestamp and actor
Rebinding Pass to New Device with Fresh Credentials
Given a verified SafeSwap for a pass When the pass is rebound to a new device Then a new device binding (device_id fingerprint) is created and associated to the pass And a fresh QR/barcode and access token are generated And previous identifiers for the pass no longer resolve to an active resource And the new credentials are usable for check-in within 5 seconds And audit logs link old and new identifiers under a single SafeSwap operation ID
Server-Side Scanner Validation Rejects Revoked Codes
Given an online scanner configured for server-side validation And a QR/barcode that was revoked by SafeSwap When the code is scanned Then the validation endpoint responds within 2 seconds with a revoked outcome And attendance is not recorded or incremented And the scan event is logged with reason "revoked" and SafeSwap operation ID
Offline Scanner Grace Policy and Fraud Window Controls
Given a scanner operating offline with a last revocation sync timestamp And a configurable offline grace window (default 5 minutes) When a revoked code is scanned during offline mode Then the scan is rejected if the code exists in the local revocation list And if the code is not locally known revoked, the scan is tentatively accepted only within the grace window And upon reconnect, the scanner revalidates all tentative scans within 60 seconds And any scans found revoked are marked invalid and capacity restored, with audit events emitted
Idempotent SafeSwap Operation with Transactional Integrity
Given a SafeSwap request is submitted with an Idempotency-Key When the same request is retried one or more times within 10 minutes Then exactly one revocation and rebind occurs And all responses return the same SafeSwap operation ID and final state And no duplicate invoices, bookings, or attendance records are created And system state across booking, attendance, and invoicing is either fully updated or fully rolled back with no partial state
Revocation Event Propagation and Integration Delivery
Given a SafeSwap revocation and rebind completes When emitting integration events Then pass.revoked and pass.rebound events are delivered to subscribed integrations within 10 seconds with signed payloads And undelivered webhooks are retried with exponential backoff for at least 24 hours And integrations using old tokens or secrets cannot authenticate after revocation And all events are traceable to the SafeSwap operation ID in logs
User Feedback and Staff Visibility of Deactivation
Given a user completes SafeSwap When the operation succeeds Then the user interface displays confirmation that the old device is deactivated and the pass is bound to the new device And an email/SMS confirmation is sent within 1 minute And the staff dashboard shows the old device status as deactivated and the new credentials as active And the SafeSwap operation appears in the pass history with timestamp and actor
Staff Approval Workflow for High-Value Events
"As a studio manager, I want to review high-risk device change requests so that valuable classes aren’t accessed through shared passes."
Description

Create an approval queue for SafeSwap requests that exceed risk or value thresholds. Staff can view request context (risk signals, recent activity, upcoming high-value bookings), approve/deny with required reason codes, and set temporary holds. Include SLA timers, notifications to staff channels, and escalation rules. Provide role-based access controls, an audit sidebar, and bulk actions for event surges. On decision, notify the client and continue or halt the SafeSwap flow accordingly, preserving a seamless experience. Admins can configure which products/events require approval.

Acceptance Criteria
Threshold Configuration and Queue Intake
Given an admin with Configuration permission selects specific products/events as "Requires Approval" and saves changes When a client submits a SafeSwap request for a configured product/event Then the request is placed into the Approval Queue, the SafeSwap flow is paused, and an audit entry is created with actorId, timestamp (ISO 8601), and ruleId applied Given an admin updates approval rules When the change is saved Then the new rules apply only to requests created after the save timestamp, and existing queued requests retain their prior rule context in the audit log Given a product/event is not configured for approval and the request risk score is below the threshold When a SafeSwap request is submitted Then the request auto-approves without entering the queue and the audit log records auto-approval with reason "Not in scope/Below threshold" Given a request meets any configured risk/value threshold (e.g., event price >= configured limit or risk score >= threshold) When the request is created Then it is routed to the Approval Queue with priority set according to the matched rule and visible in the queue within 3 seconds
Context Panel and Audit Sidebar Visibility
Given a staff user opens a queued request When the Request Context panel loads Then it displays risk signals (device change indicator, geo-IP delta, recent verification channel, failed attempts count), last 30 days activity summary (logins, swaps), upcoming high-value bookings (name/date/price), current pass/payment status, and waitlist status Given a staff user views the Audit Sidebar When the request timeline renders Then it shows an immutable, chronological log of events (verification steps, queue time, SLA start, notifications sent, decisions, holds) with actorId, action, timestamp (ISO 8601), and metadata payload sizes truncated but retrievable via expand Given a staff user lacks permission to view sensitive PII When the context panel loads Then sensitive values (email/phone last 2 digits shown only) are masked while non-PII risk signals remain visible
Approval Workspace Role-Based Access Control
Given role permissions are configured When a user with Approver or Admin role opens the Approval Queue Then they can view all queued requests and access decision controls; a Viewer can only read; any other role receives a 403 and no data is returned Given a Viewer attempts to approve/deny/hold a request When they click a decision control Then the controls are disabled and a tooltip shows "Insufficient permissions"; no state change occurs; an audit entry records the denied attempt Given a user’s role changes mid-session When they attempt an action requiring higher privilege Then the system revalidates permissions server-side and allows or blocks accordingly, recording the outcome in the audit
Approve/Deny With Reason Codes and Client Notification
Given a queued request is opened by an Approver When they click Approve or Deny Then a required Reason Code dropdown (from an admin-configured list) is enforced; the decision cannot be submitted without a reason; optional free-text notes are allowed up to 500 chars Given an Approve decision is submitted When the server persists the decision Then the client is notified via in-app and the configured channel (email or SMS) within 30 seconds, the SafeSwap flow resumes, old pass is revoked, new pass is issued, and all steps are recorded in the audit with correlationId Given a Deny decision is submitted When the server persists the decision Then the client is notified via in-app and the configured channel within 30 seconds with a generic message, the SafeSwap flow is halted, the existing pass remains active, resubmission is blocked for 24 hours unless staff override is applied, and all details are audited Given a decision is recorded When the request is re-opened Then the decision status, reason code, actorId, and timestamp are visible and immutable
Temporary Holds With Expiry and Resume/Halt Behavior
Given an Approver opens a queued request When they apply a Temporary Hold with a selected duration between 15 minutes and 24 hours and a required reason Then the request is locked from decision by others, the client sees a "Under Review" status, and the SLA is paused until the hold is released or expires Given a hold is active When it expires Then the request automatically returns to the queue, the SLA timer resumes from where it paused, staff are notified in the designated channel, and an audit entry records expiry Given a hold is active When an Approver manually releases or extends it Then the new state is reflected immediately, client status updates accordingly, and the change is audited
SLA Timers, Staff Notifications, and Escalations
Given SLA policies are configured (e.g., Priority: 30m, Standard: 2h) When a request enters the queue Then an SLA countdown starts, is visible in the UI (mm:ss), color-coded by state (green/amber/red), and pauses during holds Given a request’s SLA reaches 75% of allotted time When the threshold is crossed Then a warning notification with request link is sent to the configured staff channel(s) (e.g., Slack/email) and recorded in the audit Given a request breaches SLA When the countdown hits 0 Then an escalation notification is sent to the next-tier group/on-call, the request is flagged "Breached" in the UI, and escalation details are logged with delivery status Given an admin updates notification or escalation targets When saved Then subsequent notifications use the new targets and the configuration change is audited
Bulk Actions for Event Surge
Given multiple queued requests exist for the same event or filter criteria When a staff user selects 2–200 requests and chooses Bulk Approve, Bulk Deny, or Bulk Hold (with a single reason code) Then each request is processed independently, progress is displayed, partial failures are surfaced with error reasons, and a summary report is shown upon completion Given a bulk action is executed When processing completes Then client notifications are sent per-request according to decision type, and each request’s audit log contains a reference to the bulk operation id and the actorId Given bulk processing is underway When the user navigates away and returns Then the system resumes showing live progress and final results without duplicating actions
Full Audit Trail & Export
"As an owner, I want a detailed audit of device changes so that I can investigate disputes and meet compliance requirements."
Description

Record an immutable audit log for all SafeSwap lifecycle events: initiation, verification attempts, risk decisions, approvals/denials, revocations, token rotations, and completion. Each entry includes timestamp, actor (user/staff/system), IP/device metadata, and correlated pass/booking IDs. Provide search, filters, and export in CSV/JSON with signed hash chains to detect tampering. Respect data retention policies and PII masking rules, and integrate with existing reporting so studios can resolve disputes and demonstrate compliance.

Acceptance Criteria
Lifecycle Event Logging Completeness
Given a SafeSwap progresses through its lifecycle (initiation, verification attempt, risk decision, staff approval or denial, revocation, token rotation, completion) When any of these events occurs Then an audit entry is appended within 1 second of the event And the entry includes: timestamp (UTC ISO 8601), eventType, actorType (user|staff|system), actorId (nullable for system), sourceIp, userAgent/deviceId, passId, bookingId, swapId (correlationId), outcome (if applicable), previousHash, hash, signature And the entry is immutable (no updates or deletes via any UI/API) And entries are linked by previousHash and ordered by timestamp And the entry is retrievable by swapId, passId, bookingId, or actorId via audit search
Tamper-Evident Hash Chain Verification
Given a contiguous sequence of audit entries for a swap or time window When hash-chain verification is executed via the verification endpoint or CLI Then each entry's hash validates against its normalized payload and previousHash And the first entry of each daily chain links to a daily anchor signed with the platform key And any modification, insertion, or deletion causes verification to fail and reports the failing index and reason And the verification response includes pass|fail, verified range, and a checksum of the verified set
Audit Log Search and Filter Functionality
Given an authorized staff user opens the Audit Logs When they filter by date range, eventType, outcome, actorType, actorId, sourceIp, passId, bookingId, or swapId Then results include all and only entries matching the filters And results are sorted by timestamp desc by default and support asc/desc sorting And pagination returns consistent totals and page sizes And p50 response time <= 1.5s and p95 <= 3s for queries over 100k entries within a 30-day range
CSV/JSON Export with Signed Integrity
Given a user with export permission selects a date range and filters When they export as CSV or JSON Then the export contains only matching entries and includes standard fields plus previousHash, hash, and signature And CSV includes a header row; JSON is an array of objects with consistent field order And timestamps are UTC ISO 8601; numeric and boolean types are preserved in JSON And PII masking and role permissions are applied to exported data And each file includes an export manifest containing filters used, row count, file checksum, and signature And exports exceeding 250k rows are chunked into sequential files (each <= 100 MB) and delivered via async download
Role-Based PII Masking and Access
Given a signed-in user views or exports audit logs When the user has permission Audit.ViewFullPII Then email, phone, and IP/device fields are displayed unmasked When the user lacks Audit.ViewFullPII Then email is masked (first 2 chars + domain), phone is masked (last 4 digits only), IP is masked to /24, and deviceId is hashed and truncated And clients can only access their own audit entries And masking is consistently enforced across UI, API, and exports And masking transformations are deterministic for the same input within a 24-hour window
Data Retention and Purge Enforcement
Given a platform- or studio-level retention period is configured (e.g., 24 months) When the daily retention job runs Then audit entries older than the retention window are purged or PII fields are anonymized per policy And purge/anonymization operations are recorded as audit events And search and export do not return purged data And hash-chain verification passes for remaining entries because chains are anchored per day And retention settings are visible and read-only for users without Admin.Config permissions
Reporting Integration and Cross-Linking
Given staff use the Reporting module When they open the SafeSwap Audit report Then they can query audit logs using the standard reporting filter panel And results support drill-through to pass, booking, and swap detail pages and back-links from those pages to related audit entries And the reporting API exposes a /reports/safeswap-audit endpoint with equivalent filters and authorization And export actions invoked from Reporting use the same export engine and respect permissions and PII masking
Rate Limiting & Abuse Controls
"As a platform, I want to throttle and detect abusive swap attempts so that security is preserved without impacting legitimate users."
Description

Apply per-user, per-IP, and per-pass rate limits for SafeSwap requests and OTP attempts with cooldowns, exponential backoff, and optional CAPTCHA after repeated failures. Detect patterns such as credential stuffing or scripted swaps, auto-block abusers, and alert admins. Include configuration for limits by plan/tier, observability metrics, and safe fallbacks (support contact path) when limits are hit. Ensure concurrency controls to prevent race conditions when multiple swap attempts are made in quick succession.

Acceptance Criteria
Per-User SafeSwap Request Rate Limiting
Given a Standard-plan user, when they initiate more than 5 SafeSwap requests within a rolling 15-minute window, then requests beyond the limit are rejected with HTTP 429, include Retry-After seconds, and display a support contact link. Given a Pro-plan user, when limits are configured to 15/15min, then enforcement reflects the configured value within 2 minutes of config change. Given a user under the limit, when they submit a SafeSwap request, then the request proceeds to verification with no rate-limit headers present. Given a configuration fetch failure, when rate-limiting parameters cannot be loaded, then system applies safe default limits (5/15min) and logs reason_code="config_fallback".
Per-IP and Per-Pass Combined Limits
Given any source IP, when more than 100 SafeSwap/OTP endpoint hits occur within 5 minutes, then subsequent requests from that IP return HTTP 429 for the remainder of the window and include a CAPTCHA challenge after the 3rd 429. Given a single pass_id, when more than 1 SafeSwap is initiated for that pass within 24 hours, then subsequent attempts respond HTTP 429 with reason_code="per_pass_limit" and no state changes occur. Given limits are reached, when the window elapses, then the counters reset and requests are processed normally.
OTP Attempt Cooldowns with Exponential Backoff and CAPTCHA
Given a user entering OTP, when 3 incorrect OTP attempts occur within 10 minutes, then a 60-second cooldown is enforced and the UI displays a countdown timer. Given continued failures after cooldowns, when the 4th, 5th, and 6th attempts fail, then cooldown doubles each time (120s, 240s, 480s) up to a max of 15 minutes. Given 6 failed attempts within 30 minutes, when the user attempts another OTP, then a CAPTCHA must be solved successfully before processing the OTP. Given a successful OTP verification, when it completes, then all failure counters and CAPTCHA requirements reset for that user and pass.
Abuse Pattern Detection and Auto-Blocking
Given credential stuffing behavior, when a single IP requests OTP for 20 or more distinct accounts within 10 minutes, then the IP is auto-blocked for 24 hours and subsequent requests return HTTP 403 with reason_code="auto_block_ip" and a support link. Given scripted swaps behavior, when more than 5 SafeSwap initiations for the same account occur from 3+ distinct IPs within 10 minutes, then SafeSwap is locked for that account for 60 minutes and the account owner is notified via email. Given admin block/allow lists, when an entry is added or removed, then enforcement takes effect within 5 minutes and is recorded in the audit log with actor, timestamp, and change details. Given a support override, when a blocked entity is unblocked via admin UI, then the block is lifted immediately and the action is fully audited.
Plan/Tier-Based Limit Configuration and Validation
Given the admin limits UI, when an admin updates per-plan values (per-user, per-IP, per-pass, OTP backoff, CAPTCHA thresholds), then the system validates ranges, rejects invalid inputs with clear errors, and persists versioned configs with audit records. Given a successful config update, when services fetch configuration, then new limits are applied within 2 minutes without downtime and include policy_version in logs. Given a missing plan configuration, when a user under that plan makes requests, then default global limits apply and a warning log is emitted with reason_code="missing_plan_config".
Observability, Logging, and Admin Alerting
Given rate-limit or abuse actions, when they occur, then metrics are emitted: rate_limited_count, otp_fail_count, captcha_required_count, blocks_issued_count, concurrent_swap_conflict_count with tags {endpoint, plan, masked_ip, policy_version} at 1-min resolution. Given any auto-block event, when it triggers, then an alert is sent to the admin channel within 2 minutes containing summary (top IPs, affected accounts, rule fired). Given structured logging, when a limit is enforced, then a log entry includes request_id, hashed_user_id, masked_ip (/24), pass_id, rule_id, policy_version, and decision="enforce". Given SLO monitoring, when rate_limited_count/total_requests > 3% for 10 consecutive minutes, then an incident is opened and on-call is paged.
Concurrency Controls and Safe Fallback Support Path
Given two or more SafeSwap requests for the same pass within 1 second, when processed, then exactly one succeeds and others return HTTP 409 with message "Swap in progress" and no duplicate revocation occurs. Given idempotency keys, when the same key is reused within 24 hours, then the API returns the original result with no side effects and includes Idempotency-Replayed: true. Given high load (500 RPS) on the swap endpoint for 10 minutes, when tested, then zero double-swaps are observed and p99 latency remains under 800 ms. Given a user is rate-limited or blocked, when they receive the response, then it includes a support URL and unique case code that pre-populates a support ticket; staff can perform a one-time override after human verification.

PayGate Entry

If a scan detects unpaid or failed charges, present a one‑tap checkout that supports cards, wallets, and stored methods. On success, the pass activates immediately and the attendee clears the gate—converting would‑be turnaways into revenue without holding up the line.

Requirements

Real-time PayGate Scan Decisioning
"As a front-desk staff member, I want the gate scan to instantly tell me whether to let someone in or trigger payment so that I can keep the line moving without manual checks."
Description

Processes a QR or NFC scan at the gate to instantly determine entry eligibility by validating booking status, pass state, outstanding balances or failed charges, membership entitlements, and class capacity. Integrates with booking, payments, and membership services to return one of four outcomes: clear entry, present PayGate checkout, deny entry, or require staff override. Enforces idempotency and concurrency safety to prevent duplicate charges on rapid rescans. Targets sub-300 ms P95 decision latency to keep lines moving, with graceful degradation and minimal offline caching for short connectivity drops. Emits structured audit logs and analytics events for traceability and optimization.

Acceptance Criteria
Eligible attendee clears gate
Given a valid QR/NFC scan with an active booking or pass, no outstanding balances or failed charges, valid membership entitlements, and available class capacity When the gate device submits the scan for decisioning Then the decision response is "clear_entry" And the gate opens without presenting payment UI And a structured audit log and analytics event are emitted with outcome="clear_entry" And measured decision latency meets P95 < 300 ms over recent production scans
PayGate checkout for unpaid or failed charges
Given a QR/NFC scan associated with an attendee who has an unpaid fee or a previously failed charge for the scanned class When the system evaluates eligibility Then the decision response is "present_checkout" including supported payment methods {card, wallet, stored_method} and the exact amount due And upon successful payment, the attendee’s pass activates and the gate receives a "clear_entry" signal without requiring a rescan within 2 seconds And retries reuse a single idempotency key so that only one payment intent exists for the scan session And audit logs and analytics capture the checkout shown and conversion with a non‑PII payment reference
Deny entry when not eligible or capacity full
Given a scan where the attendee lacks a valid booking or entitlement and purchase is not possible due to full capacity or policy restrictions When the system evaluates eligibility Then the decision response is "deny_entry" with a machine‑readable reason code in {capacity_full, no_entitlement, expired_pass, banned_account} And no payment UI is presented and the gate remains closed And an audit log and analytics event are emitted with the reason code And decision latency meets P95 < 300 ms
Require staff override on risk or dependency issues
Given a scan that matches a policy requiring manual review (e.g., risk, identity mismatch, age restriction) or a critical dependency is unavailable within the decision time budget When the system evaluates eligibility Then the decision response is "require_staff_override" with a reason code in {risk_review, identity_mismatch, age_restriction, dependency_unavailable} And no payment is attempted and the gate device shows a staff prompt And an audit log includes correlation identifier, reason code, and dependency timings And decision latency meets P95 < 300 ms
Idempotency and rapid rescan protection
Given the same attendee token is scanned multiple times within a short interval for the same class, or the client retries due to network uncertainty When the system processes scans and any payment attempts Then only one payment intent is created and at most one capture can occur for the class session And subsequent scans return the current decision state without creating duplicate charges And audit logs link all attempts via a shared correlation identifier
Capacity integrity under concurrency
Given finite class capacity When multiple attendees scan and/or initiate checkout concurrently Then total admitted plus successful purchases never exceed capacity And excess attempts receive "deny_entry" with reason=capacity_full (or checkout is not offered) And updates to capacity are atomic across concurrent requests
Resilience, minimal offline caching, and eventing
Given transient connectivity drops or partial dependency outages When dependencies cannot be reached within the decision time budget needed to maintain P95 < 300 ms Then the service degrades gracefully by returning "require_staff_override" rather than blocking the gate And minimal cached decisions may be used only for previously validated entries within a very short freshness window and with signature validation And every decision outcome emits a structured audit log and analytics event with fields including timestamp, outcome, reason (if any), scan_id, hashed_attendee_id, class_id, and latency_ms
One‑Tap Checkout at Gate
"As an attendee, I want a fast one‑tap way to pay at the gate so that I can enter immediately without holding up others."
Description

Presents a minimal-friction payment sheet when an unpaid or failed charge is detected, prefilled with the attendee’s stored methods and supporting cards, Apple Pay, and Google Pay. Optimizes for a single confirmation tap while still handling tax, discounts, and dynamic pricing. Supports SCA/3DS where required without blocking the line by using in-sheet challenges and fast fallbacks. Provides a keypad fallback for manual card entry if wallets are unavailable. Ensures localized currency, language, and receipt preferences based on the attendee profile and venue settings.

Acceptance Criteria
Unpaid Scan Prompts One‑Tap Checkout and Instant Gate Clearance
Given a gate scan detects an unpaid booking or failed charge for a recognized attendee When the gate device sends the attendee ID and class instance ID Then a payment sheet opens within 500 ms showing the outstanding amount and eligible payment options Given the attendee completes payment successfully When the processor confirms authorization or capture Then the attendee’s pass status updates to Active within 2 seconds and the gate receives a clear signal within 500 ms of activation; no rescan required Given payment fails or times out When failure is returned Then the sheet displays an actionable error and offers retry and alternate payment options without dismissing the sheet
Stored Methods Prefill and Single Confirmation Flow
Given the attendee has one or more stored payment methods When the payment sheet opens Then the default stored method is preselected and all necessary fields are prefilled so only a single confirmation tap is required to pay Given multiple stored methods are available When the user opts to change method Then the user can switch methods in ≤ 2 taps and the new selection persists as default for the next checkout Given issuer policy requires CVV re-entry When the sheet is displayed Then only CVV is requested inline and the user can complete payment with one additional field and one confirmation tap
Wallet Payments: Apple Pay and Google Pay One‑Tap
Given the device supports Apple Pay or Google Pay and a wallet is provisioned When the payment sheet opens Then the corresponding wallet button is visible and enabled and displays the final total in the local currency Given the user authorizes via wallet When wallet authorization succeeds Then the payment completes without additional on-sheet forms and the pass activates immediately per gate rules Given the device lacks wallet support or no wallet is provisioned When the sheet opens Then wallet buttons are hidden and card/stored methods are shown without errors
SCA/3DS In‑Sheet Challenge with Fast Fallback
Given the transaction requires SCA/3DS When the user confirms payment Then the authentication challenge renders inline within the sheet without navigating away from the flow Given frictionless SCA is permitted by the issuer When payment is confirmed Then authorization completes without an explicit challenge and the pass activates Given a challenge succeeds When the issuer returns success Then authorization finalizes and the pass activates; median additional time introduced by SCA flow is ≤ 8 seconds Given a challenge fails or times out When the issuer returns failure or timeout Then the sheet remains open, shows a clear message, and offers an alternate method or retry within 2 taps; the scan UI remains responsive
Manual Card Entry Keypad Fallback
Given no stored methods are available and no wallet is supported or provisioned When the payment sheet opens Then a keypad-first card entry is presented for card number, expiry, CVC, and ZIP/postal optimized for fast input Given the user enters card details When validation runs Then Luhn, expiry, and basic AVS checks validate client-side with inline errors shown within 100 ms for invalid fields Given payment is submitted When network latency occurs Then the sheet displays a non-blocking progress state and offers cancel or retry without freezing; retries can be initiated within 1 tap
Accurate Pricing: Taxes, Discounts, Dynamic Pricing
Given venue tax rules, attendee eligibility, and any discount codes When the payment sheet opens Then line items and totals reflect correct taxes, discounts, and dynamic pricing as of scan time with no discrepancy > $0.01 between display and authorization amount Given a discount code is entered or auto-applied from profile When applied Then the total updates within 300 ms and the code validity state is shown inline Given dynamic pricing changed in the last 60 seconds When the sheet prepares to display Then it fetches fresh pricing before rendering and shows the updated total used for authorization
Localization: Currency, Language, Receipt Preferences
Given attendee profile and venue settings provide locale and currency When the payment sheet opens Then UI text, number/date formats, and currency code/symbol match those settings; currency used for authorization matches the displayed currency Given payment succeeds When the transaction completes Then a receipt is sent via the attendee’s preferred channel (email/SMS/in-app) within 60 seconds and is localized to the sheet language Given locale data is missing When the sheet opens Then default to the venue locale and log a non-blocking warning without interrupting payment
Secure Method Vaulting & Wallet Support
"As a returning attendee, I want my preferred payment method available by default so that I can complete payment with a single tap."
Description

Enables secure storage of customer payment methods using PSP tokenization and network tokens to achieve SAQ A scope, with opt-in consent and clear controls to remove or set defaults. Supports automatic card updates via account updater services, mandate capture for regions requiring SCA, and wallet passes for Apple Pay and Google Pay. Ensures per-tenant configuration for allowed methods, retry rules, and surcharge compliance. Provides device- and platform-agnostic presentation, preserving a consistent checkout regardless of who scans or which device initiates payment.

Acceptance Criteria
PSP Tokenization & SAQ A Scope Enforcement
Given a customer adds a new card during checkout When card details are entered Then card data is collected only via PSP-hosted fields and never transmitted to or stored on ClassTap servers or client code outside the hosted fields And the PSP returns a payment_method_token and, where supported, a network_token And ClassTap persists only token identifiers and non-sensitive metadata (brand, last4, exp_month, exp_year) with no PAN or CVV And database and log scans show no full PAN/CVV present And tokenized methods can be used to create charges without exposing PAN to ClassTap, preserving SAQ A scope
Explicit Consent and Method Management Controls
Given a customer is checking out at PayGate When the option to save a payment method is presented Then the save/consent control is unchecked by default with clear language describing future charges And selecting it records consent with timestamp, user ID, IP/device, and policy version And deselecting it prevents vaulting Given a customer has one or more saved methods When they manage methods Then they can set a single default method, remove any method, and changes take effect within 60 seconds across all devices And removed methods become immediately unusable for future charges and are no longer displayed in checkout
Account Updater Service Integration
Given a saved card is updated by the issuer via account updater When the PSP provides updated credentials Then the vaulted token remains valid and ClassTap reflects the new expiry and network token where applicable within 48 hours And subsequent charges use the updated details without user action Given updater fails to refresh an expiring/expired card When the card becomes invalid Then the method is flagged as needs_update, hidden from one-tap defaults, and the user is prompted to re-authenticate or add a new method
SCA and Mandate Capture for Regulated Regions
Given a customer in an SCA-required region saves a card for future use When the setup is initiated Then a PSP SetupIntent (or equivalent) is used to perform 3DS as required and a mandate reference is captured and stored with the token Given a subsequent merchant-initiated transaction at PayGate When the saved method is charged Then the transaction is submitted as MIT with mandate reference and appropriate SCA indicators And if the PSP requires customer authentication, the checkout prompts an on-device challenge; on failure, no charge is captured and no pass is activated
Apple Pay and Google Pay Support with Wallet Pass Activation
Given the device and region support Apple Pay or Google Pay and the tenant has enabled wallets When the PayGate checkout is displayed Then the appropriate native wallet button is shown as a primary option and respects tenant-allowed methods When the customer completes a wallet payment Then a network tokenized method is stored (subject to consent), the wallet instrument is marked as such, and the attendee pass is activated immediately And an add-to-wallet pass option is presented; if added, the pass status reflects Active within 5 seconds Given the device or region does not support wallets Then wallet options are not displayed
Per‑Tenant Allowed Methods, Retry Rules, and Surcharge Compliance
Given tenant configuration defines allowed methods, retry policy, and surcharge rules When PayGate checkout is rendered Then only allowed methods appear in the presented list in configured order Given a surcharge is configured and permitted in the transaction jurisdiction When a method that incurs a surcharge is selected Then the exact surcharge amount and new total are shown before confirmation and require explicit customer acknowledgment Given a surcharge is prohibited in the jurisdiction Then no surcharge is applied or displayed Given a payment attempt fails at the gate When retry rules permit additional attempts Then the system retries per configured backoff/order using next allowed methods and never exceeds the tenant’s max retry count, with each attempt logged
Device-/Platform-Agnostic Checkout Consistency and Activation Latency
Given any supported scanner or staff device (iOS, Android, Web) initiates PayGate for the same attendee and tenant When the checkout is shown Then the same method list, ordering, default selection, labels, and pricing (including surcharges) are presented, except for native wallet UI differences And 95th-percentile time from scan to checkout render is ≤ 1.5 seconds And 95th-percentile time from PSP success to pass activation is ≤ 1.0 second And telemetry events record device, render time, payment method, and activation latency for auditability
Instant Pass Activation & Gate Control
"As a studio owner, I want passes to activate the moment payment succeeds so that the gate opens without delay and my attendance records stay accurate."
Description

Immediately activates the attendee’s pass and booking upon successful charge capture and dispatches a gate-open signal to the scanner or turnstile. Implements idempotent success handling across client callbacks and payment webhooks to avoid race conditions. Targets sub-200 ms P95 from payment success to gate signal, with queued retries if hardware acknowledgements fail. Ensures transactional consistency between booking, attendance, and payment records, and reconciles state on late-arriving webhooks or partial failures. Surfaces clear on-screen confirmation and audible feedback for both attendee and staff.

Acceptance Criteria
Gate Opens Within 200ms After Successful Payment
Given an attendee scan triggers a one-tap payment And the payment provider returns a definitive captured-success (callback or webhook) When the system receives the first success signal Then the attendee’s pass and booking are activated atomically And an attendance admission record is created with admitted_at timestamp And a gate_open command is dispatched to the targeted device And the elapsed time from success signal receipt to gate_open dispatch is <= 200 ms at P95 measured over >= 1000 transactions with >= 50 concurrent sessions And no gate_open is dispatched prior to the transactional commit
Idempotent Success Handling Across Callback and Webhook
Given the system may receive duplicate or out-of-order success events (client callback, payment webhook) When multiple success events with the same payment_intent_id arrive within a 24-hour window Then exactly one activation occurs, exactly one attendance admission is recorded, and at most one gate_open command is dispatched And subsequent duplicate events are acknowledged with 2xx and logged as duplicates without side effects And no additional charges are created or captured And emitted domain events include an idempotency_key equal to payment_intent_id
Hardware Acknowledgement with Queued Retries
Given a gate_open command was dispatched to a specific scanner/turnstile When no hardware acknowledgement is received within 500 ms Then the system enqueues retries with exponential backoff for up to 5 attempts over 60 seconds And upon receiving a single acknowledgement, all pending retries are canceled and the command is marked succeeded once And if all retries exhaust without acknowledgement, the gate_open is marked failed, a clear on-screen prompt advises staff to assist, and no further open commands are sent without a new scan And all retry attempts and outcomes are logged with a shared correlation_id
Transactional Consistency of Booking, Attendance, and Payment
Given a successful charge capture must activate access When persisting pass activation, booking confirmation, attendance admission, and payment capture Then writes occur in a single atomic transaction or unit-of-work And either all related records are committed with a shared correlation_id and consistent timestamps or none are committed And no gate_open command is dispatched unless the transaction commits successfully And on persistence error, user-facing state remains unpaid/not admitted and a clear error is surfaced
Late Webhook Reconciliation Without Duplicate Gate Open
Given a client callback already activated the pass and opened the gate When a late payment_success webhook arrives for the same payment_intent_id Then the system reconciles state, confirms paid/admitted status, and appends an audit log entry with source=webhook And no additional activation, attendance admission, or gate_open is executed And the webhook is acknowledged with HTTP 2xx within 200 ms
Clear On-screen and Audible Feedback
Given payment success and pass activation When the gate_open command is dispatched Then the attendee-facing display shows a green success confirmation with attendee name, class, and admit status within 300 ms And an audible success tone plays once at the scanner And on failure (decline, timeout, or hardware failure), a red error state displays a concise reason and a distinct audible tone within 500 ms And success/failure screens auto-dismiss within 5 seconds or on the next scan
Concurrent Scans and Payment Success Race Conditions
Given two near-simultaneous scans and payment attempts for the same attendee and class When both flows report success (in any order, including duplicates) Then exactly one pass activation and one attendance admission occur And only one gate_open command is dispatched to the device And the second flow returns an already_activated response with no additional gate signal And under 50 concurrent race scenarios the system completes each flow within 1 second at P95 without deadlocks
Decline Handling, Retry & Grace Entry
"As a studio manager, I want failed payments to be handled quickly with safe retries or a temporary grace option so that we minimize bottlenecks and still collect revenue."
Description

Provides clear, actionable messaging for declines with a one-tap retry path using an alternate stored method or wallet. Applies smart throttling and timeouts to avoid blocking the line, with configurable policies for staff override and time-limited grace entry that triggers an automatic post-visit collection link. Captures decline reasons for analytics, enforces velocity and attempt limits to prevent abuse, and sets account flags for repeated delinquency. Sends optional SMS/email pay-now links and updates the booking hold or waitlist status based on outcome.

Acceptance Criteria
Gate Decline Messaging and One‑Tap Retry
Given a gate scan detects an unpaid booking or a declined charge, When the payment attempt fails, Then display a user-friendly decline banner within 300 ms including a normalized reason (e.g., Insufficient funds, Card expired) and a one-sentence resolution hint. Given the decline banner is shown, When the attendee taps Retry, Then present one-tap options for all stored cards and available device wallets (Apple Pay/Google Pay), preselecting the most recently successful method. Given a retry is initiated, When authorization completes or times out, Then surface success/failure within 5 seconds or mark Timeout at 5 seconds with clear messaging, and log the attempt with timestamp and method. Given a retry succeeds, When authorization is approved, Then activate the pass and open the gate within 500 ms and dismiss decline UI. Given retries occur during a single gate session, When two total declines occur, Then disable further on-device retries for that session and present Send Link and Staff Assist actions.
Smart Throttling and Timeout at Gate
Given a payment authorization request is in flight, When no gateway response is received within 5 seconds, Then cancel the attempt as Timeout, return UI to actionable state, and enable one additional retry. Given an attendee is in the gate payment flow, When cumulative time spent exceeds 30 seconds or two failed attempts occur, Then stop further on-device attempts and present fallback actions (Grant Grace if enabled, Send Pay Link, Move Aside) without blocking the scanner. Given throttling stops on-device attempts, When fallback options are shown, Then auto-reset the scanner UI to scan-ready within 3 seconds while maintaining a background link to the attendee record for staff. Given staff mode is open, When throttling triggers, Then show an explicit prompt with buttons for Grant Grace, Send Link, and Reject Entry.
Staff Override and Time‑Limited Grace Entry
Given a declined or unpaid attendee, When a staff member with Override permission taps Grant Grace Entry, Then the gate opens and a Pending Payment record is created with a configurable grace window (default 12 hours; min 30 minutes; max 48 hours). Given grace entry is granted, When the grace window starts, Then automatically send a pay-now link via the attendee's preferred channels per studio settings and create a collection task. Given payment is captured within the grace window, When authorization succeeds, Then update the booking to Confirmed, clear grace flags, and prevent late-fee application. Given the grace window expires without payment, When the deadline passes, Then flag the account as Delinquent, mark the booking Unpaid Post-Visit, and schedule follow-up according to policy. Given a staff override is used, When the action is taken, Then audit-log staff ID, timestamp, location, device ID, and selected reason.
Decline Reason Capture and Analytics
Given a payment decline occurs at gate or via pay link, When the gateway returns a code and message, Then map it to a normalized taxonomy and store code_raw, reason_normalized, card_brand, last4, wallet_type, AVS/CVV results, attempt_context, and latency in analytics. Given a decline is logged, When reporting runs hourly, Then dashboards show decline counts, rates, and top reasons by location/class/device with under 60-minute latency. Given decline reason is Card expired, When the attendee has a stored card, Then prompt for card update and suppress further retries with that card for 24 hours. Given decline reason is Insufficient funds or Do not honor, When a device wallet is available, Then prioritize the wallet as the default option on the next retry. Given decline data is stored, When records are persisted, Then ensure no full PAN or CVV is stored and all identifiers are tokenized.
Velocity and Attempt Limits
Given an account accrues failed attempts, When failures reach 3 within 24 hours or 5 within 7 days, Then block further instant retries for 12 hours and display a Contact Support recommendation. Given an account has 2 unpaid post-visit items within 30 days, When a new gate decline occurs, Then flag the account as Repeated Delinquency, alert staff at scan, and disable grace entry by default (staff may override). Given device-level anomalies are detected, When a single device records 10 declines within 15 minutes, Then require a staff PIN to allow any further attempts from that device for the next 30 minutes. Given velocity limits block a retry, When a staff member with Override permission authorizes an exception, Then allow one additional retry and capture a full audit trail of the exception.
Optional SMS/Email Pay‑Now Links
Given a decline at gate, When the attendee selects Send Link or throttling triggers fallback, Then send a unique, signed, single-use pay-now link via opted-in channels within 5 seconds and record delivery status (queued, sent, delivered, failed). Given a pay-now link is opened, When the page loads, Then render in under 2 seconds on a 4G connection and display amount due with one-tap wallet and stored methods. Given no payment occurs after link issuance, When 15 minutes elapse, Then send one reminder if configured, respecting quiet hours and opt-out preferences. Given a pay-now link is paid, When authorization succeeds, Then activate the pass/booking immediately and mark the gate check as cleared on next scan. Given link security is enforced, When the link is accessed after expiry (default 48 hours), Then show an expired state and allow requesting a new link without revealing payment details.
Booking Hold and Waitlist Status Updates
Given a class has limited capacity and payment fails at gate, When grace entry is not granted, Then set booking status to Hold – Payment Required for 10 minutes and, upon hold expiry, offer the spot to the next waitlisted attendee if capacity is needed. Given a successful payment is captured via retry or link, When authorization succeeds, Then update booking to Confirmed, rescind pending waitlist offers if not accepted, and mark attendance as Present. Given payment is not received by hold expiry, When the 10-minute window passes, Then cancel the booking, notify the attendee, and proceed with the waitlist offer per policy. Given an attendee remains unpaid after attending via grace, When end-of-day reconciliation runs, Then update status to Unpaid Post-Visit and apply late-fee rules if configured, and flag the account for follow-up.
Receipts, Invoices & Accounting Sync
"As a studio owner, I want receipts and accounting to update automatically after a gate payment so that my books stay accurate without extra admin work."
Description

Generates itemized receipts immediately after payment and attaches them to the booking and attendee profile. Issues or updates invoices as required, applies correct tax treatment, and records entries in the studio’s ledger. Syncs transactions and refunds to connected accounting systems and supports end-of-day reconciliation reports that tie gate entries to payments and attendance. Emits analytics for conversion rate, time-to-clear, declines, and recovered revenue to inform optimization and forecasting.

Acceptance Criteria
Immediate Receipt Generation and Attachment Post PayGate Payment
Given a PayGate payment is successfully captured for a scanned attendee When the transaction confirmation is received from the processor Then an itemized receipt is generated including: receipt_id, booking_id, attendee_id, class_id, line_items[description, qty, unit_price, amount], subtotal, tax_lines[name/rate/jurisdiction, amount], discounts, total, currency, payment_method_brand, last4 or wallet type, transaction_id, timestamp (ISO8601, studio local tz), studio_name, venue And the receipt is attached to the related booking and the attendee profile And the receipt is accessible in the staff dashboard and attendee portal/email link And generation and attachment complete within 3 seconds at p95 And duplicate receipts are not created on retries (idempotent by transaction_id)
Invoice Creation or Update for PayGate Charges
Given the studio setting "Record invoices for gate payments" is enabled And a PayGate payment succeeds When an open invoice exists for the attendee and session Then the invoice is updated with a line item matching the receipt (description, qty, unit_price, tax) And the invoice status becomes Paid if fully covered or Partially Paid if not When no open invoice exists Then a new invoice is created and marked Paid upon capture with totals equal to the receipt within $0.01 rounding And the invoice PDF is attached to the booking and visible in exports And invoice numbering follows studio sequence without gaps or duplicates
Correct Tax Treatment and Itemization
Given venue location, class/product tax category, and studio tax configuration When calculating totals for a PayGate checkout Then applicable taxes are computed using the correct jurisdiction rates effective at the time of sale And tax-exempt attendees/items are excluded and annotated on receipt/invoice And tax rounding follows jurisdiction rules and matches across receipt, invoice, and ledger within $0.01 And tax lines are displayed with names and rates (e.g., NYS 4.0%, NYC 4.5%) And out-of-jurisdiction classes with no nexus apply 0% tax
Ledger Recording for Studio Accounting
Given a PayGate payment is captured When recording the transaction in the studio ledger Then a balanced journal is created with debits=credits including appropriate accounts: Cash/Processor Clearing, Sales/Revenue (per item category), Tax Payable, Discounts, and Processor Fees (if available or estimated) And each line includes date, reference transaction_id, booking_id, attendee_id, class_id, amount, currency, and account code And on full or partial refund, a reversing journal entry is posted and linked to the original And ledger writes complete within 5 seconds at p95 And journal creation is idempotent by transaction_id and refund_id
Sync Transactions and Refunds to Connected Accounting Systems
Given a studio has an active accounting integration and mapped accounts/tax codes When a PayGate transaction is captured or refunded Then the transaction is synced to the external system as the appropriate object(s) (e.g., Sales Receipt/Payment/Credit Note) with correct accounts and tax codes And the sync occurs within 2 minutes at p95 after capture/refund And retries with exponential backoff occur for up to 24 hours on failure without creating duplicates (idempotent external keys) And sync status and last error are visible in the admin with timestamps And transactions with mapping or validation errors are flagged as Action Required and notification is sent to studio admins
End-of-Day Reconciliation Report: Gate Entries to Payments and Attendance
Given the business day closes at studio local midnight or an on-demand trigger is executed When generating the reconciliation report for a date range or single day Then the report includes counts and totals for scans, successful payments, declines, refunds, net revenue, tax collected, and attendance And each gate entry is linked to a receipt/invoice and an attendance record; unmatched items are listed with reason codes And financial totals reconcile: sum(receipts) - refunds = net revenue; sum(tax_lines) = tax payable; attendance count = cleared gate entries And the report is exportable to CSV and PDF and is available in the dashboard within 5 minutes of close And running the report for the same period returns identical results (deterministic)
Analytics Emission for Conversion, Time-to-Clear, Declines, and Recovered Revenue
Given PayGate scans and checkout events occur When events happen (scan_unpaid_detected, checkout_presented, payment_succeeded, payment_failed, refund_processed, gate_cleared) Then analytics events are emitted within 5 seconds with properties: studio_id, location_id, class_id, attendee_hash, device_id, amount, currency, tax_amount, discount_amount, payment_method_type, time_to_clear_ms, decline_code, attempt_count, recovered_revenue_flag And conversion rate = payment_succeeded / scan_unpaid_detected is computable by class, location, and day And p50 and p95 time_to_clear metrics are computable and displayed in the dashboard And data passes schema validation and excludes raw PAN or sensitive tokens (PCI safe) And analytics are queryable in the dashboard within 15 minutes of event time

Smart 2FA

Risk‑based step‑up verification at the door. Mismatched devices, unusual locations, or repeated failures trigger a quick second factor (OTP, push, or biometric on device). Adds security only when needed, preserving a fast experience for trusted attendees.

Requirements

Adaptive Risk Scoring Engine
"As a studio owner, I want high-risk sign-ins and check-ins automatically challenged so that my classes stay secure without slowing trusted attendees."
Description

Build a real-time risk engine that evaluates authentication, booking, payment, and door check-in events to decide whether to require step-up 2FA. Inputs include device fingerprint, IP reputation, geo and studio geofence proximity, time-of-day and velocity checks, account tenure and role, historical failures, and payment-related risk signals. Provide configurable thresholds, allow/block lists, and explainable reason codes for each decision. The engine must respond in under 150 ms to preserve a fast UX, expose a policy API for other ClassTap services, and emit standardized events for analytics and auditing.

Acceptance Criteria
Policy API Contract, Versioning, and Performance
Given a valid POST to /v1/policy/decision with event_type in {auth, booking, payment, check_in} and required signal fields When the request is processed Then respond 200 within p95 ≤ 100 ms and p99 ≤ 150 ms over a 10k-request run at ≥ 500 RPS mixed workload and include body {decision ∈ [allow, step_up, deny], risk_score ∈ [0,100], reason_codes[], config_version, request_id, latency_ms}. When required fields are missing Then respond 400 with error_code = 'bad_request' and a list of missing fields. When a downstream signal provider times out Then respond within 150 ms with decision = 'step_up' and include reason_codes ['signal_timeout:<provider>']. When X-API-Version header is provided Then echo the same version in response and apply the corresponding policy; when absent Then apply latest stable policy. When Idempotency-Key header is reused within 2 minutes for an identical payload Then return the same decision and request_id.
Runtime Config: Thresholds, Allow/Block Lists, Precedence, Audit
Given an authorized admin updates risk thresholds or signal weights via the config service When saved Then a new config_version is created and applied to all new decisions within 60 seconds. When an account_id, device_id, or ip matches the denylist Then decision = 'deny' regardless of computed risk and reason_codes include 'denylist_match:<type>'. When an account_id, device_id, or ip matches the allowlist Then decision = 'allow' unless also on denylist; denylist precedence > allowlist; reason_codes include 'allowlist_match:<type>'. When a submitted config is invalid (schema or range) Then the change is rejected with 422 and no new config_version is activated. When any config change is applied Then an AdminConfigChanged audit event is emitted with actor, change_summary, before/after, timestamp, and config_version.
Explainable Decisions: Reason Codes and Contributors
Given any decision response Then it includes reason_codes drawn from a controlled vocabulary and an 'explanation' string summarizing top contributors. And it includes top_contributors[] of up to 5 entries with {signal, weight, observed_value}. When the same normalized inputs are evaluated under the same config_version Then risk_score and reason_codes are identical. When a signal is missing or stale Then reason_codes include 'signal_missing:<signal>' and default weights are applied.
Authentication Step-Up on Device/Geo/Failures
Given device_fingerprint != last_successful_device within 30 days And geo_distance_from_last_success > 100 km within 24 hours When an auth decision is requested Then decision = 'step_up' And reason_codes include ['device_mismatch','geo_anomaly']. Given failed_auth_attempts_last_15m ≥ 3 And current attempt shares device or IP with prior failures When an auth decision is requested Then decision = 'step_up' And reason_codes include ['recent_failures','attack_velocity']. Given ip_reputation ∈ {'poor','unknown'} And device_first_seen < 48h When an auth decision is requested Then decision = 'step_up' And reason_codes include ['ip_reputation_low','new_device'].
Trusted Context Fast-Path (No Step-Up)
Given device_fingerprint == last_successful_device And ip_reputation ∈ {'good','neutral'} And (distance_to_studio_geofence ≤ 150 m Or distance_to_home_geo ≤ 20 km) And account_tenure_days ≥ 90 And failed_auths_past_30d = 0 When an auth decision is requested Then decision = 'allow' And reason_codes include ['trusted_context'] And no step-up challenge is initiated.
Payment Risk Step-Up Before Booking Confirmation
Given psp_risk_score ≥ 80 Or (avs_result = 'mismatch' And cvv_result = 'fail') Or payment_attempts_last_10m ≥ 3 When a payment decision is requested Then decision = 'step_up' And reason_codes include any of ['psp_high_risk','avs_mismatch','cvv_fail','payment_velocity']. Given payer account_id is on allowlist When a payment decision is requested Then decision = 'allow' And reason_codes include ['allowlist_match:account']. Given card_bin_country != device_geo_country And geo_to_billing_distance_km > 1000 When a payment decision is requested Then decision = 'step_up' And reason_codes include ['geo_bin_mismatch'].
Standardized Decision Events for Analytics and Audit
Given a decision is returned Then a DecisionEvaluated event is emitted within 2 seconds to the analytics bus with fields {event_id, request_id, timestamp, event_type, account_id, decision, risk_score, reason_codes, top_contributors, config_version, latency_ms, policy_version}. And delivery is at-least-once and events reach the warehouse within 5 minutes for ≥ 95% of cases. And PII is redacted: ip_address hashed with salted SHA-256 and device_id replaced with a stable surrogate key. When emission fails transiently Then retry with exponential backoff up to 5 attempts and on final failure route to DLQ; internal logs include 'event_emit_failure' with correlation to request_id.
Step-up Method Orchestration & Fallback
"As an attendee, I want the system to use the quickest available second factor on my device so that I can verify and get in fast."
Description

Implement an orchestrator that selects and triggers the optimal second factor per context and device, prioritizing push to the ClassTap app and WebAuthn/passkeys (biometrics) when available, then TOTP, then SMS/email OTP as fallbacks. Respect user preferences, device capabilities, and network conditions. Include deterministic fallback sequencing, challenge expiration, rate limiting, replay protection, and anti-abuse controls (e.g., SIM-swap heuristics, number recycling checks). Provide reusable UI components/SDKs for web, mobile, and kiosk, with localized copy and accessibility support.

Acceptance Criteria
Optimal Method Selection with Preferences and Capabilities
Given step-up verification is required due to elevated risk And the user has registered Push (ClassTap app), WebAuthn/passkey, TOTP, SMS, and Email factors And device/browser/app capabilities and network health are available When the orchestrator selects a method Then it must prefer Push or WebAuthn (highest tier), then TOTP, then SMS, then Email, unless the user’s explicit safe preference dictates otherwise And it must exclude methods unsupported or unhealthy on the current device/session And it must not initiate more than one method concurrently And it must log decision reason codes (selected_method, excluded_methods, user_pref_applied, capability_checks) And it must avoid SMS/Email when Push or WebAuthn are available and healthy
Deterministic Fallback, Timeouts, and Single Active Challenge
Given a step-up challenge is in progress When a method fails, is declined, or does not complete within 60 seconds (configurable) Then the orchestrator cancels the active challenge and advances to the next method in the defined sequence without requiring the user to restart authentication And at most one active challenge token exists at any time And each challenge expires in 5 minutes (configurable) and cannot be redeemed after expiration And users may manually switch to any available stronger method from the UI without creating parallel challenges And all transitions are auditable with timestamps and reason codes
Rate Limiting and Abuse Mitigation
Given repeated step-up attempts for the same user, device, or IP When failures or excessive requests occur Then enforce rate limits: max 5 challenges/user/15 minutes, 3/device/10 minutes, 10/IP/10 minutes (all configurable) And apply exponential backoff on repeated failures (e.g., 30s, 60s, 120s) And after 10 failed challenges in 24 hours, require the strongest available factor (Push or WebAuthn) and temporarily disable SMS/Email for 24 hours And user-facing responses remain generic to prevent account enumeration And all throttling and block decisions are logged with correlation IDs
SIM-Swap and Number Recycling Risk Handling
Given SMS OTP is being considered or is the only available method And telco/heuristic signals indicate SIM change or porting within the last 7 days, or the number is newly recycled or untrusted When step-up is initiated Then SMS is deprioritized in favor of stronger factors; if none exist, require out-of-band verification (e.g., email link plus TOTP) or deny with a recovery path And if SMS is used, enforce stricter limits (1 OTP/10 minutes, 3/day), 7+ digit OTP, and 2-minute expiry (configurable) And record the risk reason and display non-sensitive guidance to the user
Localized, Accessible Step-Up UI Components/SDKs
Given integrators use the provided web, mobile, and kiosk SDKs When rendering step-up screens, prompts, timers, and errors Then UI meets WCAG 2.1 AA (focus order, labels, contrast ≥ 4.5:1, keyboard/touch accessibility, ARIA live announcements for countdowns) And supports at least 12 locales with runtime switching, pluralization, and RTL layouts And all text (including method names and errors) is fully localizable; no hard-coded strings in SDK samples And mobile uses platform biometric prompts; kiosk offers large-touch OTP keypad and clear cancel/try-another-method actions And kiosk SDK stores no secrets or PII at rest and auto-clears the screen after 30 seconds of inactivity And SDKs expose callbacks for challenge_started, method_switched, challenge_succeeded, challenge_expired, throttled
Challenge Replay Protection and Cryptographic Binding
Given any step-up challenge is issued When a response is received Then the server validates a single-use, unpredictable nonce bound to user_id, session_id, device_id (if available), and method, with a 5-minute expiry And rejects and logs any replay or binding mismatch as replay_attempt And Push confirmations must be signed by a device-held key; WebAuthn assertions must validate RP ID, origin, counter, and challenge; TOTP must validate code and time step with ±1 window And all successful verifications rotate the nonce and invalidate any outstanding challenges for the session
2FA Enrollment & Recovery
"As a user, I want easy setup and recovery options for my second factors so that I’m never locked out of my bookings."
Description

Offer guided flows to enroll and manage second factors during onboarding and from Account Security: register passkeys/WebAuthn, link phone/email for OTP, set a default factor, and generate single-use backup codes. Enforce minimum enrollment (e.g., at least two factors) based on studio policy. Provide secure recovery options for lost devices, including backup code use, recovery contacts, and support-assisted recovery with identity proofing and full audit trail. Sensitive changes (phone/email) require re-authentication and step-up verification.

Acceptance Criteria
Onboarding: Enroll Passkey (WebAuthn)
Given a new user at the Security step of onboarding on a WebAuthn-capable device When the user selects "Add passkey" and successfully completes the WebAuthn ceremony Then the credential ID, public key, and AAGUID are stored server-side (no private keys), bound to the user, and marked verified And the passkey appears in the factor list with a device label and creation timestamp And an immediate test authentication with the passkey succeeds without fallback And the event is recorded in the audit log with user ID, IP, user agent, timestamp, and outcome
Link Phone/Email for OTP with Verification and Rate Limits
Given the user opens Account Security > Add OTP factor When the user inputs a phone number (E.164) or email and requests verification Then a 6-digit OTP is generated, single-use, expires in 5 minutes, and is delivered (SMS/voice within 10s; email within 60s) And no more than 5 OTP send attempts and 10 verify attempts are allowed per 30 minutes per contact method; excess attempts are blocked with a clear message And upon correct code entry within validity, the method is marked verified and added to enrolled factors And unverified methods cannot be set as default or used for step-up And PII is stored masked (phone last 4, email local part masked) and never logged in full
Set Default Factor and Fallback Order
Given the user has 2 verified factors When the user selects a default factor and reorders fallbacks Then the selected default is persisted and used first for future step-ups And unavailable defaults automatically fall back to the next available verified factor with clear prompts And unverified or disabled factors cannot be selected as default or included in fallback order And the change is recorded in the audit log with old/new default and actor
Enforce Minimum Enrollment per Studio Policy
Given the studio policy requires a minimum of 2 enrolled factors and allows [passkey, OTP, backup codes] When the user attempts to complete onboarding or disable a factor that would bring them below the minimum Then the action is blocked and the UI clearly lists the remaining requirements and permitted factor types And when the user meets or exceeds the minimum, completion is allowed immediately without additional manual approval And policy evaluation is studio-scoped; changes to studio policy take effect for subsequent sessions within 5 minutes
Generate and Manage Single-Use Backup Codes
Given the user initiates "Generate backup codes" from Account Security and re-authenticates When generation succeeds Then exactly 10 unique, 8-character codes are created, displayed once, and offered for download (.txt) and print And previously issued backup codes (if any) are revoked immediately upon regeneration And each code can be used once to satisfy step-up or recovery, after which it is invalidated And the system displays the remaining count and the last used timestamp And all code generation, download, and use events are recorded in the audit log (never storing the raw codes)
Sensitive Changes Require Re-auth and Step-Up
Given the user attempts to change a phone number or email used for OTP When the user has not re-authenticated in the past 5 minutes Then the system requires re-authentication (password or passkey) plus a step-up using a different enrolled factor before permitting the change And upon change, confirmation notices are sent to both old and new contacts with undo link valid for 30 minutes And after 3 failed step-up attempts, the change flow is locked for 15 minutes and an alert is shown And the audit log records the change request, verification methods used, confirmations sent, and final outcome
Account Recovery: Recovery Contact and Support-Assisted Workflow
Given the user configures a recovery contact by entering the contacts email and the contact accepts via a signed invitation When the user initiates account recovery due to lost devices Then the user can choose to verify via a valid backup code or request recovery contact approval; either path enforces a 24-hour cooldown before granting access And support-assisted recovery requires an agent to complete identity proofing (ID document check + knowledge-based verification), collect consent, and obtain supervisor approval And successful support-assisted recovery results in a forced password reset, revocation of existing sessions and factors, and issuance of new backup codes And every step of the recovery flow (user, contact, and support actions) is captured in a tamper-evident audit trail with timestamps, actor IDs, artifacts, and outcomes
Trusted Device & Session Remembering
"As a frequent attendee, I want the system to remember my trusted devices so that I’m not challenged every time."
Description

Allow users to mark a device as trusted after successful step-up, issuing signed, expiring trust tokens bound to a privacy-preserving device identifier. Honor studio-configured trust durations and revoke trust automatically on password change, risk score spikes, or admin action. Sync trust across web and mobile where appropriate, while preventing abuse via attestation signals, IP/network heuristics, and server-side checks that can override trust when risk is elevated.

Acceptance Criteria
Trust Device After Successful Step‑Up
Given a user is prompted for step‑up verification due to elevated risk And the user completes OTP, push, or biometric challenge successfully within the allowed window When the user selects “Trust this device for [configured duration]” and confirms Then the system issues a cryptographically signed, expiring trust token bound to the device’s privacy‑preserving identifier And the token is persisted client‑side and registered server‑side with device alias, attestation type, issuance time, and expiry And an audit log entry is recorded with user ID, device alias, IP hash, attestation result, and duration And subsequent logins from this device bypass step‑up while the token remains valid and risk is below threshold
Honor Studio‑Configured Trust Duration
Given a studio has configured trust duration to N days (within platform min/max bounds) And a device has a valid trust token issued for that studio When the user logs in from that device with normal risk Then step‑up is not required until the token expires And after N days (± allowable clock skew) the token is treated as expired And the next login requires step‑up unless the device is re‑trusted
Automatic Trust Revocation on Sensitive Events
Given a device has an active trust token When the user changes their password Or the account risk score crosses the high‑risk threshold for two consecutive evaluations Or an admin revokes trust for the user or specific device Then the trust token is invalidated server‑side immediately And subsequent logins from that device require step‑up And an audit event is recorded with a revocation reason code and actor
Risk‑Based Override of Trusted Status
Given a user presents a valid, unexpired trust token for a device And the current attempt originates from that device but with unusual network/location producing a risk score ≥ StepUpThreshold When the user attempts to access protected actions (login, payment, check‑in) Then a step‑up challenge is required despite the trusted status And on successful completion the existing token remains if still valid, otherwise a new token is issued per policy And on failure or timeout access is denied and a security event is logged
Cross‑Platform Trust Sync on Same Physical Device
Given a device was trusted in the native mobile app with successful attestation When the same user logs in via the mobile browser or supported webview on the same physical device And device attestation/identifier equivalence is confirmed per policy Then the trusted status is honored and step‑up is skipped under normal risk And trust is not propagated to different physical devices or to desktop browsers without attestation equivalence And trust is never shared across different user accounts
Device‑Bound Token Integrity and Privacy
Given a trust token is presented during authentication When the server validates the token Then the token signature verifies against active or grace‑period keys via kid And the bound device identifier matches the current device’s attestation‑derived identifier And the token contains no raw PII and only non‑reversible identifiers And any tampered, expired, or mismatched token is rejected and logged with reason And a platform hard cap (e.g., 90 days) limits token TTL regardless of studio settings
Abuse Prevention and Rate Limits for Trusting Devices
Given an account attempts to create or rotate trusted devices When more than M unique device IDs are trusted within a rolling 24‑hour window Then additional trust grants are blocked and the user is notified And trust cannot be granted from environments failing device attestation or flagged as high‑risk proxies/VPNs per heuristics And rapid trust grant/revoke toggling is throttled server‑side And devices detected as rooted/jailbroken are denied trust when policy prohibits
Door Check-in Verification Flow
"As a front-desk manager, I want risky check-ins verified instantly so that only the right people enter without delaying the line."
Description

Integrate Smart 2FA with the in-studio check-in experience (QR scan, kiosk, roster tap). When risk is elevated (unknown device, location mismatch, repeated failures), trigger an immediate step-up on the attendee’s device (push/passkey) or present a kiosk prompt. Provide a staff override path that captures reason and evidence, and a fallback one-time code entry if the phone is unavailable. Optimize end-to-end verification to average under 3 seconds to avoid front-desk bottlenecks and update attendee status in real time to prevent double-bookings and ghost check-ins.

Acceptance Criteria
Known Device Fast Check-in Under 3 Seconds
Given an attendee with a trusted device fingerprint and usual location within 5 km of historical check-in geo and zero failed attempts in the last 24 hours And the attendee holds a valid booking for the current class instance When the attendee scans their QR at the door or staff taps the roster to check in Then the system completes check-in without step-up and displays a success confirmation within ≤3 seconds end-to-end And the backend writes the attendee status = "present" within ≤300 ms And all connected clients (kiosk, staff app, attendee app) reflect the updated status within ≤2 seconds And across 100 consecutive low-risk check-ins on reference hardware/network, the average end-to-end time is ≤3 seconds and p95 ≤5 seconds And the event is audit-logged with latency metrics and device/location trust indicators
Elevated Risk Triggers Step-Up Authentication
Given risk conditions are met: unknown device fingerprint OR location mismatch beyond the configurable threshold (default 50 km) from usual check-in geo OR ≥2 failed check-in attempts within 10 minutes And the attendee has a valid booking for the current class instance When the attendee attempts check-in via QR scan, kiosk, or staff roster tap Then the system initiates a step-up challenge within ≤1 second And if a capable device is registered, a push/passkey (WebAuthn/biometric) prompt is sent to the attendee device And if no capable device is registered, a kiosk step-up prompt is presented And the check-in state is set to "pending verification" until completion And the risk reason code(s) and trigger details are recorded in the audit log
Push/Passkey Step-Up Completes Check-in
Given a step-up challenge is active via push notification or passkey/biometric on the attendee's registered device When the attendee approves the push or completes the passkey/biometric within 15 seconds Then verification succeeds and the attendee is marked "present" immediately And total time from initial scan/tap to "present" is ≤5 seconds at p95 on reference network conditions And the method used (push, passkey, biometric) and device identifier hash are stored in the audit log And if the challenge is not approved within 15 seconds or is rejected, the attempt is canceled and the system offers fallback options (OTP/backup code entry or staff override)
Phone Unavailable — Kiosk One-Time/Backup Code Fallback
Given step-up is required and the attendee indicates their phone is unavailable When the attendee selects "Use one-time/backup code" at the kiosk or staff app Then the system prompts for a single-use backup code associated with the attendee account (8-character alphanumeric, case-insensitive) And a valid unused backup code marks the attendee "present" and consumes the code And invalid codes are rejected with a generic error; attempts are limited to 3 before a 2-minute lockout is applied And if no backup codes exist or are exhausted, the UI prompts to engage staff for identity verification and override And all attempts, lockouts, and outcomes are recorded in the audit log
Staff Override Captures Reason and Evidence
Given an attendee with a valid booking cannot complete verification and an authorized staff member is signed in with override permission When the staff selects "Override" during check-in Then a modal requires selection of a reason (from a predefined list) and a free-text note (minimum 10 characters) And at least one evidence item is required (camera capture of ID or file attachment); evidence hash and metadata are stored And upon confirmation, the attendee is marked "present" with an "override" flag visible on the roster and attendee detail And the audit log records staff ID, timestamp, reason, note, evidence hash, and original risk trigger(s) And overrides are blocked for blocked/suspended accounts with a clear error message And per-staff override rate is limited to ≤5 per hour; exceeding the limit requires manager approval
Real-Time Status Update and Double-Booking/Ghost Check-in Prevention
Given a check-in completes successfully via any path (trusted, step-up, backup code, or staff override) When the system updates attendance Then the backend sets status = "present" and acquires a class-instance attendance lock within ≤300 ms And re-check-in for the same class instance within 10 minutes is prevented with a clear message And check-in to any overlapping class instance (time overlap ≥1 minute) is blocked until the current class is released And no attendance record is created if verification fails or times out (prevents ghost check-ins) And all connected clients receive the updated status/event within ≤2 seconds via real-time channel And an AttendanceUpdated event is emitted for downstream systems with idempotency keys
Admin Policy Controls & Geofencing
"As a studio owner, I want to tune when step-up is required so that I balance security and speed for my clientele."
Description

Deliver a policy console for studio admins to configure risk thresholds, required/allowed factors, remember-device duration, challenge frequency, and bypass rules by role (attendee, instructor, admin). Enable geofenced studio locations and trusted networks to reduce unnecessary prompts on-site while escalating off-site anomalies. Provide policy templates (Strict, Balanced, Fast), versioning with change logs, instant propagation, and a test mode to observe impact before enforcement.

Acceptance Criteria
Policy Console: Risk Thresholds & Factor Controls
Given an org admin with PolicyManage permission When they set risk threshold to Medium (0.6), required factors to [OTP or Push], allowed factors to [OTP, Push, Biometric], remember-device duration to 30 days, and challenge frequency to "once per device per 24h" Then the UI saves successfully, the policy persists, and a GET /policy returns the exact values just set And invalid inputs (e.g., remember-device > 365 days, empty allowed factors) are rejected with descriptive errors and nothing is saved And an immutable audit event is recorded with editor, timestamp, old→new values, and reason And enforcement uses the new values on the next authentication event for the org
On‑Site Trusted Zones (Geofence + Network) Behavior
Given a studio location geofence of 50 meters around GPS (37.7749, -122.4194) and a trusted SSID "StudioNet" are configured When an attendee authenticates within the 50m radius while connected to "StudioNet" and has a remembered device (age < configured duration) and risk score < policy threshold Then no step‑up challenge is prompted and a decision log notes "trusted zone: geofence+network" When the same attendee authenticates 2km away or on an untrusted network with the same device Then a step‑up challenge is required if risk score ≥ threshold, and the decision log notes location/network reasons And location evaluation tolerates GPS jitter up to ±15m and falls back to network trust if GPS is unavailable
Policy Templates: Strict, Balanced, Fast
Given the templates Strict, Balanced, and Fast are available with documented defaults When an admin previews a template Then a diff view shows changes vs current policy (per field) without applying them When the admin applies a template Then all template fields are set atomically, a new policy version is created, and the change log records the template name and field deltas And admins can customize any field after applying a template, resulting in another version with its own change log entry
Versioning & Change Log with Rollback
Given a policy has versions v12 (current) and prior versions When an admin rolls back to v10 Then v13 (copy of v10) becomes the new current version, preserving v12 in history, and an audit entry records who, when, and why And change logs for every version include editor, timestamp, field-level diffs, and optional comment And read-only users can view the full version history and diffs
Instant Propagation of Policy Changes
Given a new policy version is saved at time T0 When enforcement nodes receive updates Then 95% of authentication decisions reflect the new policy within ≤30 seconds of T0, and 100% within ≤60 seconds And stale caches on edge nodes are invalidated within ≤30 seconds And existing authenticated sessions are unaffected until their next step requiring policy evaluation
Test Mode (Shadow Enforcement) with Impact Observability
Given Test Mode is enabled for the org with a 100% evaluation audience When authentications occur Then policy decisions are evaluated, but no user is blocked or challenged due to Test Mode; instead, a "would challenge" flag and reason are logged And an analytics dashboard shows projected challenge rate, bypass rate, and top reasons, with filters by role, location, and device over selectable time ranges When Test Mode is turned off or set to a 10% audience Then enforcement switches immediately per the configured audience, and logs clearly indicate mode and audience at decision time
Role‑Based Bypass Rules Enforcement
Given bypass rules are configured: Instructors bypass step‑up when in trusted zones and risk < Medium; Admins never bypass; Attendees follow default policy When an Instructor authenticates on-site within a trusted zone with risk score 0.4 Then no step‑up is prompted and the decision log cites role-based bypass When the same Instructor authenticates off-site with risk score 0.4 Then default policy applies and a step‑up occurs if required by threshold When any user’s risk score is Critical (≥0.9) Then no bypass is honored and a step‑up is always required, with the decision log citing critical risk override
Security Audit Logs, Alerts & Analytics
"As an operator, I want visibility into verification events and outcomes so that I can adjust policies and prove compliance."
Description

Capture structured, immutable logs for auth and check-in events, risk scores, challenges issued, outcomes, overrides, and recovery actions. Provide dashboards for challenge rate, success latency, abandonment, false-positive rate, and correlation with no-shows and chargebacks. Support CSV export, API/webhook streaming to SIEM, configurable retention windows, and user data access/deletion workflows for compliance. Add anomaly alerts for spikes in failures, geo-velocity violations, and method deliverability issues.

Acceptance Criteria
Immutable Structured Security Logging
- Given an authentication or check-in event occurs, when the system writes a log entry, then an append-only structured record is created with required fields: event_id (UUIDv4), tenant_id, event_type ∈ [auth_attempt, check_in, risk_score_calculated, challenge_issued, challenge_result, override_applied, recovery_action], user_ref (pseudonymous), session_id, device_ref (pseudonymous), ip, geo (country, region, approximate_lat, approximate_lng), timestamp (ISO 8601, UTC), risk_score (0–100), risk_reasons[], challenge_method ∈ [otp, push, biometric, none], outcome ∈ [success, failure, abandoned, overridden], admin_actor (optional), error_code (optional), schema_version, record_hash, prev_record_hash. - Given logs are persisted, when integrity verification runs daily, then the hash chain and signature validation pass for 100% of records or else a failed integrity report is generated and an integrity alert is emitted within 5 minutes. - Given multi-tenant isolation, when a user with tenant A credentials queries logs, then no records from tenant B are returned (0 cross-tenant records in 100 sampled queries). - Given PII governance, when logs contain contact identifiers, then they are tokenized or masked per policy at write time and raw values are never stored in the immutable log. - Given clock skew, when events are recorded within a session_id, then timestamps are monotonic non-decreasing and stored in UTC.
Analytics Dashboards for Challenges and Outcomes
- Given a date range is selected, when viewing the dashboard, then the following metrics are displayed and numerically accurate within ±1% against a sampled SQL verification: challenge_rate, challenge_success_latency (p50, p95), abandonment_rate, false_positive_rate, and correlation with no-shows and chargebacks (Pearson r with p-value). - Given filters (location, device type, challenge_method, tenant, cohort) are applied, when updating the view for up to 30 days and ≤10M events, then charts and KPIs refresh within ≤3 seconds at the 95th percentile. - Given a KPI is clicked, when drilling down, then a paginated sample (≤100 rows) of underlying events with key fields is shown and matches the aggregate within ±1% for the sampled window. - Given streaming ingestion, when new events arrive, then dashboard data freshness (time since last processed event) is ≤5 minutes at the 95th percentile and the freshness indicator displays the lag. - Given an Export Chart action, when triggered, then a CSV of the currently filtered aggregate data is downloaded within ≤10 seconds for ≤100k points.
CSV Export of Security Events
- Given a user with Export permission selects a date range (≤31 days) and filters, when requesting CSV export for ≤10M events, then the request is queued and a downloadable link is provided within ≤10 minutes or an email with link is sent upon completion. - Given the CSV is generated, when opened, then it contains a header row and required columns with ISO 8601 UTC timestamps, UTF-8 encoding, RFC 4180-compliant quoting/escaping, and no malformed rows across a 1M-row sample. - Given large outputs, when file size exceeds 200 MB, then the export is delivered as a ZIP and chunked into parts ≤200 MB each with consistent headers. - Given PII controls, when "Redact sensitive fields" is enabled, then email/phone/device identifiers are masked/tokenized; when disabled, only pseudonymous references are present (no raw PII stored by design). - Given auditing, when an export completes, then an audit entry is recorded with requester, filters, time range, row count, file size, and completion timestamp. - Given access control, when a user without Export permission attempts export, then the action is denied and logged with reason.
Real-time SIEM Streaming via Webhooks/API
- Given a tenant configures a webhook endpoint with URL and shared secret, when events are generated, then the system delivers JSON batches (1–100 events or 1-second interval) containing schema_version, batch_id, idempotency_key, sequence, created_at, and events[], over HTTPS TLS 1.2+. - Given delivery security, when a batch is sent, then it includes HMAC-SHA256 signature and timestamp headers; the signature validates against the shared secret for 100% of deliveries in tests. - Given transient failures (non-2xx), when delivery fails, then retries occur with exponential backoff and jitter up to 10 attempts or 24 hours; 410 disables the destination; success is any 2xx. - Given ordering guarantees, when consuming within a tenant, then batch sequence is ordered and at-least-once delivery is ensured; duplicates can be de-duplicated using idempotency_key (no missing sequences in steady state). - Given observability, when streaming is active, then delivery success rate, lag, retry count, and DLQ size are exposed in metrics and the admin UI. - Given validation, when "Send test event" is clicked, then a synthetic, clearly labeled batch is delivered and visible in SIEM within ≤60 seconds. - Given pull-based integration, when using the Events API with cursor pagination, then a client can page through events in order with consistent cursors and rate limits documented.
Configurable Log Retention and Legal Hold
- Given an admin sets retention windows per event category within 30–730 days (default 365), when saved, then the configuration persists and is applied to subsequent purge jobs. - Given nightly purge at 02:00 UTC, when records exceed their retention, then they are removed from primary storage and search indexes within ≤24 hours; a purge report is generated with counts by category. - Given distributed replicas, when a purge executes, then deletions propagate to replicas within ≤72 hours; aggregates retain only anonymized, non-attributable metrics. - Given legal hold is enabled for a user or case, when purge runs, then held records are excluded from deletion until the hold is removed; the hold is auditable with reason and owner. - Given compliance, when a purge completes, then an immutable audit entry records who configured retention, what was purged, and when; the UI displays the next scheduled purge time and effective policies.
User Data Access and Deletion (DSAR) Workflow
- Given an authorized privacy role submits a Data Access request with a user identifier and date range, when processed, then a machine-readable package (ZIP with JSON/CSV) of the user's security-related records is produced within ≤24 hours for ≤5 GB and ≤48 hours for up to 20 GB, and a secure download link is provided. - Given a deletion request for a user, when two-person approval is completed, then the user's identifiable records are removed from primary stores and search indexes within ≤24 hours and from replicas within ≤72 hours; aggregates remain only in anonymized form. - Given completion, when deletion finalizes, then a non-identifying audit receipt (request_id, timestamps, approvers, scope) is recorded and provided to the requester; subsequent queries for that user return zero records. - Given access control, when a non-privileged user attempts DSAR actions, then the action is denied, logged, and no data is exposed. - Given security, when the package is downloaded, then the link requires short-lived tokens (≤15 minutes) and auto-expires after single use or TTL, whichever comes first.
Anomaly Alerts: Failures, Geo-Velocity, Deliverability
- Given baseline modeling, when authentication failures in a 15-minute window exceed max(3σ over the trailing 7-day same-hour baseline, configurable absolute threshold), then an alert is generated within ≤2 minutes including tenant, location, method, and recent trend. - Given geo-velocity checks, when a single user has consecutive events implying speed above the configurable threshold (default 900 km/h) within 60 minutes, then the event is flagged and an alert is emitted with both geo points and computed speed. - Given method deliverability monitoring, when OTP/push/email challenge delivery failure rate >5% or p95 delivery latency >10 seconds over 10 minutes, then a deliverability alert is sent with provider breakdown and suggested fallback if configured. - Given noise controls, when an alert fires, then duplicate alerts of the same type/context are suppressed for a 30-minute dedup window unless severity escalates. - Given incident workflow, when an alert is received, then it is tracked in the Alerts UI with states [Open, Acknowledged, Resolved]; acknowledging stops notifications; resolve requires a note. - Given testing, when "Simulate alert" is used, then a synthetic alert is created without affecting metrics and routes to all configured channels (email, Slack, webhook) within ≤2 minutes.

Product Ideas

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

BrandSnap Setup Wizard

Guided import from Calendly/Mindbody, auto-builds classes, prices, and branded pages in 30 minutes. Previews live booking flow before publish to cut onboarding friction.

Idea

Family Wallet & Waivers

Parents create dependent profiles, sign once, and pay from a shared wallet. Sibling discounts auto-apply and attendance syncs to each child’s roster.

Idea

SeatSense Optimizer

Visual equipment map assigns seats, prevents conflicts, and recommends optimal fills to maximize capacity. Auto-moves waitlisted clients into exact spots.

Idea

Time-Zone Trust Clock

Show every class in the viewer’s local time with automatic conversion and SMS reminders. Host sees dual-time headers to avoid embarrassing misses.

Idea

Installment Guardrails

Offer deposits and pay‑in‑X plans with automatic retries, smart dunning emails, and card updater. Failed payments trigger seat holds and easy one-tap recovery.

Idea

SnapRoles Audit Trails

Preset and granular permissions for owners, staff, and substitutes with expiring access. Every change logs to an audit trail for accountability.

Idea

LinkLock Check-In

Magic-link login and QR check‑in bind to a device, blocking link sharing. Optional 2FA at door reduces fraud and ensures only paid attendees enter.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

ClassTap Announces Frictionless Onboarding Suite to Migrate Instructors in Hours, Not Weeks

Imagined Press Article

San Francisco, CA — September 7, 2025 — ClassTap, the brandable online booking and payments platform built for independent instructors and small studios, today announced a comprehensive onboarding suite designed to take businesses live in hours instead of weeks. The new lineup harmonizes schedules, pricing, branding, and policies with minimal manual effort, empowering time‑strapped teams to switch tools without risking revenue or client trust. The onboarding suite is centered on a pragmatic promise: make ClassTap your single source of truth without double‑entry or post‑launch surprises. By pairing automated data import with live, risk‑free testing, ClassTap accelerates setup while reducing costly mistakes that can lead to no‑shows, billing issues, and client churn. Key capabilities launching today include: - SourceSync Merge: Connect Calendly, Mindbody, and calendar sources in minutes, then automatically deduplicate events and unify overlapping classes into a single master schedule. Choose a one‑time import to jump‑start setup or an ongoing sync to keep legacy tools in step while you transition. - AutoMap Fields: An AI‑assisted mapper reads existing class types, instructors, locations, capacities, and time zones, then proposes precise field matches for quick review‑and‑approve. This eliminates tedious manual mapping and prevents setup errors that create client confusion. - BrandSnap Palette: Drop in a website or logo to instantly generate an accessible, on‑brand booking theme—colors, fonts, buttons, and image treatments—with live previews as you tweak. - Pricing Translator: Automatically converts passes, packs, memberships, taxes, and discounts from your old system into ClassTap equivalents, with side‑by‑side comparisons and guidance to maintain revenue rules. - Policy Port: Import and standardize cancellation windows, no‑show rules, waivers, and terms into reusable templates that auto‑attach to classes and programs. - Test‑Drive Checkout: Run the full booking flow—reservation, waitlist auto‑fill, and payment with test cards—before you publish. Share a preview link with teammates to spot friction and fix it fast. - Launch Guard: A preflight checklist that flags time‑zone mismatches, capacity conflicts, missing prices, and broken links. Choose a soft launch to a pilot group, then go live with one click and built‑in rollback for safe, confident publishing. “Independent instructors and boutique studios are builders by nature—but switching systems can feel like rebuilding the plane in flight,” said Ava Lin, CEO and co‑founder of ClassTap. “This suite clears the runway. You bring your classes and brand, we bring automation and guardrails. Our goal is to let owners move in hours, not weeks, and make day one on ClassTap feel like day 101.” The suite is tailored for ClassTap’s core user types: - Solo Brand‑Builders who run everything themselves can stand up a polished, on‑brand booking page without hiring a designer—and avoid double‑bookings from day one. - Boutique Multi‑Instructor Studios gain centralized scheduling, consistent policies, and clean instructor/room assignments across locations. - Hybrid Streamers keep in‑person and virtual classes in sync, attach streaming links reliably, and reduce no‑shows with clear confirmations and reminders. - Pop‑Up Venue Coaches can import rotating events, manage capacity by location, and invoice on the go. - Corporate Wellness Coordinators standardize programs across client sites with dependable capacity controls and consolidated billing. - Admin‑First Studio Managers save time with bulk mapping, fast roster previews, and a preflight that catches what humans miss. For migration‑averse teams, ClassTap emphasizes transparency and control at every step. AutoMap Fields surfaces suggested mappings for approval, Pricing Translator shows changes before they go live, and Launch Guard provides a human‑readable checklist so owners understand the impact of each decision. Teams can soft‑launch to a pilot audience, collect feedback, and roll back with a click if needed. “In under a day we had three locations and six schedules unified—and our clients never saw the messy middle,” said Multi‑Location Maven Mia, owner of FlowHaus Studios. “SourceSync Merge pulled classes from two tools, AutoMap did 90% of the tedious work, and Test‑Drive let us check the experience on phone and desktop before we told anyone. We launched over a weekend without losing a single booked client.” During internal testing and early customer pilots, ClassTap saw setup time drop by more than half for typical studios and administrative time fall by up to 60% in the first month as teams eliminated double‑entry and manual cleanup. Studios also reported fewer no‑shows thanks to clearer confirmations and policy alignment, with some seeing a 30% reduction as automations kicked in. Availability and pricing - The onboarding suite is available today to all ClassTap plans at no additional charge. - Ongoing sync for SourceSync Merge supports popular calendar and scheduling platforms; additional connectors roll out on a published roadmap. - New migration guides and checklists, including a Best Practices Library for owners and front‑desk leads, are available in the ClassTap Help Center. “Switching platforms shouldn’t mean switching off your business,” added Lin. “With ClassTap, you can import what works, test what matters, and launch with confidence—and you don’t need a consultant to do it.” About ClassTap ClassTap is the brandable booking and payment platform built for independent instructors and small studios. ClassTap streamlines scheduling, invoices, and attendee management, automates waitlist‑to‑payment flows, prevents double‑bookings, and helps reduce no‑shows. Instructors report cutting administrative time by up to 60%, reducing no‑shows by around 30%, and boosting monthly revenue by approximately 15% after adopting ClassTap’s automations and best‑practice workflows. Media contact - Press: press@classtap.com - Phone: +1 415‑555‑0134 - Website: www.classtap.com/press

P

ClassTap Launches Family Programs Toolkit to Simplify Kid Enrollments and Keep Front Desks Calm

Imagined Press Article

San Francisco, CA — September 7, 2025 — ClassTap today introduced the Family Programs Toolkit, a purpose‑built set of features that makes it easy for studios to enroll children, manage guardianship, and keep safety and compliance airtight—without front‑desk chaos. The toolkit brings together guardian roles, age‑based eligibility, health profiles, family wallets, sibling discounts, and a single scan for fast multi‑child check‑in. Children’s programs operate under tougher constraints than adult classes—multiple guardians, safety notes, age limits, and school calendars. ClassTap’s Family Programs Toolkit addresses these realities so owners can welcome more families with less manual work and greater peace of mind. Core components available today include: - Guardian Roles: Define primary and secondary guardians, pickup‑only contacts, and emergency backups per child. Fine‑grained permissions govern who can book, pay, sign waivers, and receive communications. Every authorization is auditable to cut confusion and risk. - AgeSmart Eligibility: Automatically match children to age‑appropriate classes using date of birth, school grade, and prerequisites. Ineligible sessions are hidden or clearly labeled with reasons; upcoming birthdays trigger move‑up suggestions to boost retention. - HealthPass Profiles: Store allergies, medications, and emergency details in a secure profile that surfaces to instructors on rosters and at check‑in. Time‑bound documents—doctor notes, immunizations, waivers—carry expiry reminders and one‑tap update requests. - SpendGuard Wallet: A shared family wallet with per‑child spending limits, class‑type restrictions, and optional auto‑reload. Refunds and credits route back correctly and can be earmarked for a specific child. - Sibling Saver: Flexible sibling discounts that auto‑apply across drop‑ins, packs, and memberships with caps and stacking rules. Savings display transparently during checkout and on invoices. - Consent Cascade: When policies change, new waiver versions roll out to all linked guardians and dependents with a single e‑sign flow. Smart reminders and class gatekeeping ensure only compliant families can book or check in. - Family QR Check‑In: One QR for the whole family. Scan once to mark the right kids present for the right classes, even across rooms. Late/absence notes and pickup codes capture in seconds and sync to each child’s roster. “Family programming has been underserved by generic booking tools,” said Priya Nandakumar, VP of Product at ClassTap. “Parents need clarity, studios need control, and kids need safety. The Family Programs Toolkit gives each stakeholder what they need—without turning the front desk into a paper chase.” The toolkit is tailored to the workflows of Family‑Programs Frankie and cohort‑based organizers like Cohort‑Builder Casey: - Frankie can admit only age‑appropriate students, capture guardian authorizations once, and see health info at a glance—no more asking the same questions every week. - Casey can bundle 4–8 week programs, enforce prerequisites, and offer installment plans while the system handles waivers and class moves as kids age up. Early adopters report a measurable lift in operations and satisfaction: - Reduced check‑in lines as families move through with a single scan—even on busy Saturdays with mixed‑age classes. - Fewer billing disputes thanks to transparent wallet controls and itemized discounts that match what parents see at checkout. - Higher retention when AgeSmart Eligibility prompts timely move‑ups, making progress tangible and keeping kids in the right level. “Before ClassTap, we were juggling handwritten rosters and parent texts,” said Frankie Lopez, owner of Sprout Movement, a youth fitness studio in Austin. “Now, parents book for both kids in one flow, waivers live on file, and our instructors see allergies and pickup notes right on the roster. Saturday check‑in dropped from ten minutes per family to less than two.” Security and compliance are integral. Consent Cascade centralizes policy updates and tracks acknowledgment history, while HealthPass Profiles surface need‑to‑know information to the right people at the right time. Each action is logged for accountability, and permissioned roles ensure only authorized guardians can sign or pay. ClassTap’s privacy practices emphasize minimal necessary access and clear audit trails. For studios that run pop‑ups or offsite programs, Family QR Check‑In works even when connectivity is spotty, thanks to Offline Seal—short‑lived, cryptographically signed tokens that verify scans without Wi‑Fi and sync automatically when back online. Parents receive confirmations with the exact local start time, avoiding time‑zone confusion for traveling families. Availability and getting started - The Family Programs Toolkit is available today to all ClassTap customers at no additional cost. - Studios can import existing family and dependent data, waivers, and credits using Policy Port and SpendGuard Wallet migration helpers. - New templates include a Kid‑Friendly Cancellation Policy, Medication Note, and Guardian Authorization language reviewed by industry advisors. “Studios shouldn’t have to choose between a great family experience and reliable operations,” added Nandakumar. “With ClassTap, you can have both. Families feel seen, instructors feel prepared, and owners feel in control.” About ClassTap ClassTap is a brandable booking and payments platform for independent instructors and small studios. It streamlines scheduling, invoices, and attendee management; automates waitlist‑to‑payment flows; prevents double‑bookings; and helps reduce no‑shows. On average, customers report cutting administrative time by up to 60%, reducing no‑shows by around 30%, and boosting monthly revenue by approximately 15% after adopting ClassTap’s automations. Media contact - Press: press@classtap.com - Phone: +1 415‑555‑0134 - Website: www.classtap.com/press

P

ClassTap Debuts Seat Intelligence Suite to Maximize Utilization and Reduce No‑Shows for Equipment‑Based Studios

Imagined Press Article

San Francisco, CA — September 7, 2025 — ClassTap today announced the Seat Intelligence Suite, a set of seat‑level planning, optimization, and analytics tools designed for equipment‑based studios where every reformer, bike, or rower counts. The suite helps owners fill premium spots first, keep friends together, and protect safety—while reducing manual reshuffles that drain staff time and frustrate clients. At studios where equipment is inventory, a half‑empty front row or a broken reformer can derail the vibe and the bottom line. ClassTap addresses these realities with visual room builders, policy‑aware optimizers, and data‑backed pricing recommendations that keep layouts accurate, rosters balanced, and members delighted. The Seat Intelligence Suite includes: - Smart Grid Builder: Generate accurate seat maps in minutes using templates for reformer, cycling, mat, and row layouts. Drag‑and‑drop equipment, set aisles and mirrors, and auto‑number rows so every room matches reality. - PerfectFill: A policy‑aware auto‑fill engine that places clients for optimal vibe and capacity—front‑to‑back, checkerboard, or spread‑out—based on your rules. It respects blocked seats, member priority, ADA holds, and buddy reservations. - HealthLock: Seat‑level maintenance flags mark equipment Out of Service or Needs Attention. The optimizer excludes those spots and logs follow‑up tasks so no one gets assigned to a risky station. - BuddyLink: Let clients reserve adjacent seats for a friend with a temporary hold link that auto‑expires. Groups stay together, conversions go up, and staff avoids last‑minute swaps. - SwapCascade: When a premium seat opens, eligible clients receive one‑tap upgrade offers in priority order; the system then backfills vacated spots automatically to keep front rows full. - Seat Heatmap: Visual analytics reveal which seats and rows fill fastest, churn most, or carry higher no‑show risk by time and class type—informing layout tweaks and premium pricing. - AccessAssist: Designate accessible paths and priority seats; capture mobility needs at booking; and auto‑assign suitable spots first for inclusive, compliant seating. “Equipment is the heartbeat of these studios, and it deserves the same intelligence as any e‑commerce inventory,” said Marco Silva, Head of Product Design at ClassTap. “With ClassTap, owners can tune layout strategy like a control panel—fill patterns, upgrade rules, maintenance holds—so rosters optimize themselves while instructors focus on coaching.” The suite is built for studios like Resource‑Savvy Scheduler Sam’s, where utilization drives profitability, and Boutique Multi‑Instructor Studios balancing multiple rooms and teachers. Benefits reported by pilot customers include: - Higher premium‑seat capture: Front‑row occupancy improved as PerfectFill and SwapCascade prioritized demand. - Fewer pre‑class seat swaps: BuddyLink kept friends together and reduced front‑desk intervention. - Better safety and trust: HealthLock removed faulty stations from circulation and documented equipment status. - Insight‑driven pricing: Seat Heatmap highlighted hot and cold zones, informing premium add‑ons and targeted incentives. “SwapCascade is a game‑changer—our most loyal members get surprise upgrades, and the system backfills automatically,” said Sam Bennett, owner of Kinetic Cycle in Seattle. “We stopped playing musical chairs at the desk. No more awkward ‘can we move you?’ moments.” Operationally, the Seat Intelligence Suite pairs tightly with ClassTap’s core scheduling and waitlist‑to‑payment automations. When a seat opens, the platform can move a waitlisted client into the exact spot, charge automatically, and send confirmations that include location, seat number, and any special notes. On arrival, RollingQR passes verify attendance quickly, while GeoFence Gate ensures check‑ins occur at the venue, not remotely. Accessibility and inclusion are first‑class concerns. With AccessAssist, studios can define paths, set priority seats, and mark ADA holds. Clients can note mobility needs at booking; the system auto‑assigns suitable spots and displays clear roster badges for instructors—reducing last‑minute reshuffles and ensuring a welcoming experience for everyone. Availability and how to start - The Seat Intelligence Suite is available today across ClassTap plans. - New customers can import or build seat maps during onboarding, verify with Test‑Drive Checkout, and publish with Launch Guard to catch time‑zone or capacity conflicts. - Seat Heatmap analytics are included out of the box; studios can export insights to their BI stack or use built‑in recommendations for premium pricing. “Studios invest heavily in equipment and atmosphere,” added Silva. “This suite pays that investment back by keeping premium seats full, maintaining safety, and giving members little moments of delight—like a one‑tap upgrade that makes their day.” About ClassTap ClassTap is a brandable booking and payments platform for independent instructors and small studios. It streamlines scheduling, invoices, and attendee management; automates waitlist‑to‑payment flows; prevents double‑bookings; and helps reduce no‑shows. Many instructors report cutting administrative time by up to 60%, reducing no‑shows by around 30%, and boosting monthly revenue by approximately 15% after adopting ClassTap’s automations and best‑practice workflows. Media contact - Press: press@classtap.com - Phone: +1 415‑555‑0134 - Website: www.classtap.com/press

P

ClassTap Rolls Out Time‑Zone Trust Suite to End Scheduling Confusion for Hybrid and Global Classes

Imagined Press Article

San Francisco, CA — September 7, 2025 — ClassTap today rolled out the Time‑Zone Trust Suite, a coordinated set of time‑intelligence features that eliminates the mental math and calendar drift that plague hybrid and global classes. With automatic local‑time rendering, daylight‑saving protections, travel‑friendly toggles, and calendar invites that actually match reality, ClassTap helps instructors grow across regions without growing support tickets. For Hybrid Streamers and global instructors like Global‑Gig Gabby, time is the invisible friction that hurts conversions and show‑up rates. Misaligned calendars, off‑hour reminders, and DST surprises can turn excited attendees into no‑shows. ClassTap’s Time‑Zone Trust Suite solves these pain points end‑to‑end. The suite includes: - Auto‑Localize: Detects each viewer’s time zone and renders class times in their precise local time with a clear “Your time” label. Honors manual overrides for traveling users and stays consistent across web, email, and embeds. - DST Shield: Prevents one‑hour surprises by detecting sessions that cross daylight‑saving changes in any region. Pins reminders to the absolute start time, adjusts recurring series correctly, and sends pre‑change nudges so nobody shows up early or late. - Quick Flip: One‑tap toggle between Local, Host, and Original time zones on schedules, rosters, and invoices—plus a handy “in X hours” relative view. - Global Time Finder: Suggests high‑performing class windows across selected regions based on attendee distribution, past attendance, and quiet‑hour rules. Visual heat bands highlight “best” times for maximum conversion. - Timeproof Invites: Sends .ics calendar invites and updates with full VTIMEZONE/UTC data so Google, Apple, and Outlook render the same local start time. Auto‑updates on reschedules and includes join links. - QuietHour Reminders: Schedules SMS/email reminders to land within each attendee’s preferred local hours and honors do‑not‑disturb windows. Messages show the user’s local start time and auto‑adjust for DST shifts. “Time shouldn’t be a guessing game,” said Elena Park, Head of Growth at ClassTap. “We built Time‑Zone Trust so a client in London, a traveler in Bali, and a host in New York all see the same reality—and get reminders that feel considerate, not intrusive. It removes the single biggest source of accidental no‑shows for hybrid programs.” The impact shows up in the metrics that matter: clearer booking decisions, lower no‑show rates, and a better first experience for new clients. Early users report fewer “what time is that in my city?” support messages, stronger attendance at cohorts that span regions, and more confident scheduling for traveling instructors. “Once we turned on Auto‑Localize and Timeproof Invites, the ‘wrong time on my calendar’ tickets basically disappeared,” said Gabby Chen, a traveling yoga instructor who teaches across time zones. “QuietHour Reminders mean I’m not waking clients at 3 a.m., and Quick Flip is my favorite host view—I can sanity‑check times in one tap.” Time‑Zone Trust also supports business growth with planning intelligence. Global Time Finder analyzes historical attendance and client locations to recommend windows that balance reach and rest. Hybrid studios can plan sessions that catch both coasts without landing in anyone’s quiet hours, and corporate wellness programs can align with employee schedules across offices. The suite is tightly integrated with ClassTap’s booking and payment flows. When a waitlist spot opens, notifications use the recipient’s local time; when a client pays, their confirmation, receipt, and .ics reflect the same time zone rules. Instructors see dual‑time headers on rosters and can message cohorts with confidence that delivery respects each attendee’s quiet hours. Availability and setup - The Time‑Zone Trust Suite is available today for all ClassTap plans. - Existing customers can enable Auto‑Localize and QuietHour Reminders from Settings and add region preferences for Global Time Finder. - Admin guides include best practices for hybrid programs, including DST‑aware recurring schedules and quiet‑hour templates. “Great classes travel further when time becomes trustworthy,” added Park. “Whether you’re streaming from your living room or staging a global cohort, ClassTap helps you show up together, on time, every time.” About ClassTap ClassTap is a brandable booking and payments platform for independent instructors and small studios. It streamlines scheduling, invoices, and attendee management; automates waitlist‑to‑payment flows; prevents double‑bookings; and helps reduce no‑shows. Customers report cutting administrative time by up to 60%, reducing no‑shows by around 30%, and boosting monthly revenue by approximately 15% after adopting ClassTap’s automations and best‑practice workflows. Media contact - Press: press@classtap.com - Phone: +1 415‑555‑0134 - Website: www.classtap.com/press

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.