How to use: Each entry lists purpose, formula, defaults, a numeric impact example, and code location. Reservations have no court-fee math; ranking uses two parallel systems (public points + hidden Elo).
| Purpose | What members/admins see or what decision it drives |
| Formula | Plain-English math |
| Defaults | Admin setting or system default |
| Impact example | Concrete numbers → what changes |
| Code | Implementation location |
Two parallel ranking systems (CR):
| System | Visible? | Drives |
|--------|----------|--------|
| Public ranking points | Yes | Leaderboard order |
| Hidden Elo | No | Opponent/partner strength, optional club rating |
CCM reservations: No court fees or wait-queue math. Overlap is enforced by the database; availability is grid painting from existing bookings.
Part 1 — Club Rankings
CR-01. Recalculation pipeline
CR-01. Recalculation pipeline
Purpose
Rebuild all player standings after events are confirmed/completed.
Formula
Replay every completed event in event date order (then event ID). For each player row: compute performance → apply weekly cap → update point buckets → update Elo/club rating → update W/L/T and streaks. After all events: inactivity decay, volunteer/sportsmanship sync, achievements.
Defaults
Runs on full recalc or section/series recalc.
Impact example
Admin confirms Saturday ladder → all four players’ performance_points and leaderboard ranks update on next recalc.
Sportsmanship points for confirming results quickly.
Formula
If confirm within fast_confirm_hours of submission: add fast_confirm_bonus once to sportsmanship_points.
Defaults
Bonus 0 (off); window 48 hours.
Impact example
Bonus 1, confirm at 12h → +1.0 sportsmanship_points.
Code
lib/rankings/bonuses.js → tryFastConfirmBonus
CR-24. Inactivity decay
CR-24. Inactivity decay
Purpose
Penalize idle players on performance_points only (after full replay).
Formula
If enabled and days since last event > inactivity_days: periods = floor((days − threshold) ÷ 30) + 1; subtract periods × inactivity_penalty, floor at 0.
Defaults
Off. Threshold 90 days, penalty 5 per 30-day period.
Impact example
120 days idle, 42 performance points → 2 periods × 5 → 32.0 performance_points. May drop several leaderboard positions.
Each 1.0. Series with counts_toward_overall = false excluded from club-wide recalc.
Impact example
Event weight 1.5 × series 2.0 = 3.0 → score 60 yields raw adjustment +6.0 instead of +2.0.
Code
lib/rankings/series.js, recalculate.js
CR-29. Dashboard “most improved”
CR-29. Dashboard “most improved”
Purpose
Highlight players gaining points in rolling window vs all-time.
Formula
improvement = allTimeTotalPoints − rollingWindowTotalPoints (positive only); top 5.
Defaults
Uses same totalPoints and rolling months.
Impact example
All-time 40, rolling 28 → improvement 12 — appears on admin dashboard “most improved” list.
Code
lib/rankings/dashboard.js → buildClubDashboard
CR-30. Club Play → result aggregation
CR-30. Club Play → result aggregation
Purpose
Turn live game scores into ranking result rows.
Formula
Per completed game: W/L/T from scores; accumulate points for/against; doubles sets partner. Placement (KOTC & round robin): wins DESC → losses ASC → ties DESC → point differential DESC → weighted tiebreaker (club weights renormalized without placement component) → member ID. Ladder: placement from final ladder order. Substitution: Replace rewrites only unscored games (and later rounds) from the player who left to the incoming member. Completed games stay with the player who left. End session aggregates each player’s own games, so both appear in the session results.
Defaults
Feeds same engine as CSV/manual entry. Tiebreaker uses section or seasonal standings custom weights. The incoming member must be on the sport roster and not already in the session.
Impact example
Two players both 2W–0L with +2 pt diff — older player or stronger-opponent schedule can win placement via tiebreaker; both still receive full performance scores on recalc. After a substitution, each player’s wins and points come only from the games they actually played.
Break ties when two or more players share the same session record after wins, losses, ties, and point differential.
Formula
Blend win %, point differential, opponent strength, partner strength, and age handicap using club (or seasonal standings custom) weights with placement weight set to 0 and remaining weights renormalized. Higher score ranks higher. If still tied, lower member_id.
Defaults
Same weight preset as performance score (CR-02–CR-07). Opponent/partner strength from hidden ranking_skill_ratings at session date.
Impact example
Both 2W–0L, +2 pt diff — player who faced stronger opponents (higher hidden Elo field) wins placement; leaderboard still awards both weighted performance points.
*Reservations have no court fees or wait-time math. Overlap is enforced by Postgres; the grid paints occupied slots from existing rows.*
CCM-01. Effective monthly price
CCM-01. Effective monthly price
Purpose
Price shown at checkout and on billing admin.
Formula
custom_price_monthly ?? plan.priceMonthly; same for annual. Dollars → cents: round(dollars × 100).
Defaults
CCM $40/mo (up to 6 courts); Rankings-only $19/mo; Enterprise custom (optional read-only portfolio login for a property-management firm with several CCM communities). Legacy ids starter / growth / pro map to CCM economics.
Impact example
CCM list $40; tenant custom $35 → checkout charges $35/mo.
Reject bookings that don’t align to grid increment.
Formula
(startMin − openMin) % increment_min === 0 and same for end; endMin > startMin.
Defaults
Section increment_min often 30.
Impact example
Book 10:15 start with 30-min grid → rejected.
Code
lib/rules.js → validateBooking
CCM-09. Maximum booking length
CCM-09. Maximum booking length
Purpose
Cap session duration.
Formula
(endMin − startMin) ≤ max_length_min.
Defaults
Often 60–120 min per section.
Impact example
90-min request when max 60 → rejected.
Code
lib/rules.js → validateBooking
CCM-10. Advance booking window
CCM-10. Advance booking window
Purpose
How far ahead members may book.
Formula
dayDiff(todayClubDate, bookingDate) ≤ advance_days, and if the date is exactly advance_days out, club time must be ≥ advance_open_time (midnight 00:00 = no holdback).
Defaults
Often 7 days; new day opens at midnight.
Impact example
7-day advance: booking 8 days out → rejected; day 7 → allowed all day when open time is midnight. If new day opens at 8:00 PM, day 7 is rejected until 8:00 PM, then allowed.
Code
lib/rules.js → validateBooking
CCM-11. Same-day minimum lead time
CCM-11. Same-day minimum lead time
Purpose
Prevent booking slots starting too soon today.
Formula
If booking is today: startMin − nowMin ≥ min_lead_min.
Defaults
e.g. 60 minutes. Demo tenant uses 0 with fixed 9:00 AM teaching clock.
Impact example
At 2:00 PM with 60-min lead, 2:30 PM slot → closed on grid.
Code
lib/rules.js; lib/invite-rsvp.js; lib/demo.js for demo override
CCM-12. Daily reservation limit
CCM-12. Daily reservation limit
Purpose
Max bookings a member may create per sport per day (guest play does not count).
Formula
Count reservations this member created (booked_by) in the same sport on that date; block if count ≥ max_per_day. Being invited onto someone else’s booking does not count.
Defaults
Section max_per_day often 1–2.
Impact example
max_per_day = 1, second created booking same day → rejected. The same member can still play on other members’ courts that day.
Code
lib/daily-limit.js
CCM-13. Cancellation notice
CCM-13. Cancellation notice
Purpose
Require notice before canceling.
Formula
If minutesUntilStart ≤ 0 → refuse (the slot has started), even inside the 30-minute grace and even when min_cancel_min is 0. Else if minutesSince(created) ≤ 30 → grace (allow). Else if min_cancel_min ≤ 0 → allow. Else minutesUntilStart ≥ min_cancel_min (default 120).
Defaults
30-min grace; 120-min notice. Started slots cannot be cancelled.
Impact example
Cancel after start → blocked, including a booking made 10 minutes ago. Cancel 1 hour before start with 2-hour rule → blocked. Cancel 3 hours before start → allowed.
Code
lib/rules.js → validateCancellation
CCM-14. Time slot generation
CCM-14. Time slot generation
Purpose
Rows on court availability grid.
Formula
for (t = openMin; t + increment ≤ closeMin; t += increment) slots.
Monday cron sends reminders for Tuesday reservations in club timezone.
Code
lib/reminders.js
CCM-23. Stripe renewal overage line
CCM-23. Stripe renewal overage line
Purpose
Add member overage to draft Rankings subscription renewal invoices. CCM extra courts are billed via Stripe subscription quantity (court add-on price), not invoice line items — overageInvoiceEligible returns false for court-based plans.
Formula
Defaults
Only when billing_reason = subscription_cycle, status draft, tenant active or past_due, not enterprise, no custom pricing, and plan is not court-based.
Impact example
5 overage members on Rankings at $0.40 → $2.00 invoice line. CCM with 8 courts → quantity 2 on court-overage Price (2 × $5).
If higher-index (better rank) team loses, swap winner and loser positions in ladderOrder. If any team is outside the playing window, the loser of the lowest court moves to the end of the order and the first waiting team takes that spot.
Defaults
N/A.
Impact example
#101 loses to #102 → order flips → 102 now above 101. With a bench, the lowest-court loser drops behind the waiting teams and the next team steps on.
Optional length
Session target_rounds stops auto-advance after that round (tryAutoAdvanceRound → max_rounds). Blank = unlimited until End session.
Winners stay on their court. The bench is a queue replayed from signup order and results: sitters keep their place, losers of each round join the back, challengers come off the front. Avoid an immediate rematch, and avoid a court the challenger has already played when another challenger is free. Ties keep both teams. Random-mix doubles keeps the two winners and splits them onto the next two bench players (man + woman on a mixed roster).
Defaults
N/A.
Impact example
Court 1 winner stays. The next team off the bench challenges them. With 24 players and 4 courts, player 24 reaches a court instead of sitting out the night.
Optional length
Session target_rounds stops auto-advance after that round (tryAutoAdvanceRound → max_rounds).
Fixed rotation of partnerships/opponents across rounds.
Formula
Circle method — each pair of teams at most once. Waves fill up to courtCount games with no team twice in a wave. Random-mix doubles selects players with fewer games first, then minimizes repeat partners and opponents (man + woman when the roster is balanced).
Defaults
N/A.
Impact example
8 singles on 4 courts over 7 rounds is 28 unique games, each player once against each other.
Optional length
Pass targetRounds to schedule exactly N rounds; when omitted, default is ceil(teams / 2) (or ceil(players / 2) for random-mix doubles).
Each round, pair players by current session record so similar standings meet; fill all courts.
Formula
Choose who plays so games played differ by at most 1 (fewer games first; equal games, stronger record plays). Pair that group by session wins DESC, then point differential DESC. Prefer assignments with lower scoreRoundAssignment (fewer repeat partners/opponents). Random mix on a mixed roster uses man + woman partners. After all court scores are in, auto-advance like ladder/KOTC.
Defaults
Doubles path; courtCount × 4 players on court each round. Session target_rounds stops auto-advance (max_rounds). Performance-score weights still apply only after End session — Swiss does not change CR formulas.
Impact example
24 players / 6 courts → 6 games per round; after round 1, leaders play leaders (1v2, 3v4, … by standing).