Every membership perk actually reduces the charge

Every discount your tier promises — percentage off, fixed off, flat-rate per unit, free, weekly/biweekly/monthly allowances, initiation fees — auto-applies at checkout and hits Stripe as the right dollar amount. Server-authoritative, schema-validated, with eligibility gates for age, time-of-day, day-of-week, and location.

The nine surfaces that get discounted

One consistent model across every revenue-generating surface. Adding a new one takes a single registry entry and a ten-line resolver class.

Court reservations

Percentage off, fixed off, free, or an allowance of free sessions per period.

Open play

Weekly or monthly allowances — "7 free per week" is actually 7 per week, not "28 per month."

Leagues & tournaments

New plumbing. Discount applies to the full registration fee (base + division). Prize fund excluded.

Clinics & coaching

Per-surface rules for clinics, classes, and private lessons.

Ball machine rentals

Percentage off, flat discount, or allowance.

Passes

Discount applies to the pass total (Stripe fee unchanged).

Initiation fees

One-time Stripe Connect charge at activation. Waivable by admin, auto-reapplies after a configurable gap.

Future surfaces

One registry entry. The schema validator, formatter, and wizard pick it up automatically.

Six discount types, one vocabulary

TypeExampleWhat it does
Percentage off20% off court reservationsReduces the base by %, clamped to $0 minimum.
Fixed off$5 off open playReduces the base by N cents, clamped to $0.
Flat rate$30/hr after the allowance is spentReplaces the per-unit price entirely. Can be HIGHER than the public guest rate (overage often is).
FreeFree tournament entriesCharges $0, doesn't consume an allowance unit.
Allowance5 free court reservations per week, then 25% offFirst N free at the tier's chosen cadence, then the overage rate applies (any of the four types above).
Free with cap4 free passes per monthSugar for allowance with a non-overage UI treatment.

Six cadences, per-surface reset day, anchored to the club's clock

Daily, weekly, biweekly (every 2 weeks), monthly, quarterly, annually. For weekly and biweekly, each surface picks its own reset day — court reservations might roll over Monday while open play rolls over Sunday in the same tier. Biweekly windows are deterministic across clubs (anchored to a fixed reference week). Cancellation refunds the allowance to the period the cancellation happens in. DST and leap-year transitions don't drop credits.

Eligibility gates enforced server-side

Default eligibility (applies to every surface) plus per-surface overrides: minimum age (COPPA-floored at 13), maximum age, allowed hours, allowed days of week, allowed locations, and a per-tier priority booking window that lets members book that many hours earlier than non-members. Eligibility is enforced at TWO levels: at purchase, age limits BLOCK the buy (a 30-year-old can't purchase a senior tier); at booking, violations don't block — the member just pays the full guest rate and sees a friendly inline note. Owner-only — only the club owner can edit eligibility rules.

How it looks at checkout

Tournament registration$100.00
Member discount (Gold)−$10.00
Promo code SPRING25−$5.00
Virtual credit applied−$10.00
Total$75.00

Member discount is auto-applied. Promo code and virtual credit stack in that order. Result is always clamped to $0 or more.

Live tier reads — admin edits apply immediately

When an admin edits a tier, the change applies to every active member on their next billable action. No "Apply to existing members" button to remember. Drop the percentage from 25% to 20% at 9am — every reservation booked after 9am uses the new rate. Members are never locked out by an edit because purchase-time age gates are separate from booking-time eligibility checks. Every edit writes an immutable audit row with actor, IP, user agent, full before/after JSON, and a human-readable diff line.

Safe to ship — bulletproof concurrency

The unified ClubMembershipPeriodUsage table has a DB-level unique index on (member, surface, period_start). Two members racing to consume the last allowance unit can't double-consume — one wins, the other gets the overage rate. Every commit is wrapped in retry-on-not-unique semantics so concurrent bookings always serialize correctly. GIN indexes on the JSONB columns keep tier-search queries fast.

Included on Growth, Pro, and Elite

Free tier is not eligible (max_membership_tiers = 0 on Free). Growth and above get the full feature set with no add-on pricing.