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.

Guest 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 guest 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
Club credit applied−$10.00
Total$75.00

Member discount is auto-applied. Promo code and club 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.