Download PDF · Court Manager manual · Club Rankings manual · Captain manual · Marketing site

CCM Calculations Reference

Every formula in Club Court Manager™ and Club Rankings™ — with impact examples

Product
Club Court Manager™ + Club Rankings™
Operator
StoufferAI
Version
July 2026
PDF
/docs/ccm-calculations-reference.pdf
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).

Table of contents

  1. Part 1 — Club Rankings
  2. Part 2 — CCM Core
  3. Part 3 — Club Play scheduling
  4. Part 4 — Appendices

Product: Club Court Manager™ (CCM) + Club Rankings™ (CR)

Operator: StoufferAI

Audience: Club admins, operators, power users

Source of truth: lib/rankings/**, lib/billing*.js, lib/rules.js, public/js/court-grid.js

Version: July 14, 2026

For CR-only deep dive, see also CLUB-RANKINGS-SPECIFICATIONS.md.

How to read this document

Each calculation uses the same template:

| Field | Meaning |

|-------|---------|

| 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.
Code
lib/rankings/engine/recalculate.jsrecalculateFromEvents

CR-02. Win percentage component

CR-02. Win percentage component

Purpose
Reward session win rate in the performance blend (0–100).
Formula
If no games → 50. Else winRate = (wins + ties×0.5) ÷ (wins+losses+ties); component = clamp(winRate×100, 0, 100).
Defaults
Weight 40% (wins_weight).
Impact example
6W–2L → winRate 0.75 → component 75 → with 40% weight contributes 30 points to blend numerator.
Code
lib/rankings/engine/performance-score.jswinPctScore

CR-03. Placement component

CR-03. Placement component

Purpose
Reward finishing position in the event field.
Formula
If placement or fieldSize missing, or fieldSize ≤ 1 → 50. Else ((fieldSize − placement) ÷ (fieldSize − 1)) × 100, clamped.
Defaults
Weight 20% (placement_weight). Requires placement (1 = best) and fieldSize.
Impact example
1st of 8 → 100. 4th of 8 → ~57. Last of 8 → 0. Placement not entered → 50 (neutral).
Code
lib/rankings/engine/performance-score.jsplacementScore

CR-04. Point differential component

CR-04. Point differential component

Purpose
Reward scoring margin vs peers in the same event.
Formula
diff = points_for − points_against. maxAbsDiff = largest |diff| among all players in the event (min 1). Component = clamp(50 + (diff ÷ maxAbsDiff)×50, 0, 100).
Defaults
Weight 15% (point_diff_weight).
Impact example
Event max diff is 10. Player +10 → 100; 0 → 50; −10 → 0. Drives ~7.5 blend points swing at 15% weight between best and worst margin.
Code
lib/rankings/engine/performance-score.jspointDiffScore

CR-05. Opponent strength component

CR-05. Opponent strength component

Purpose
Reward beating a strong field (uses hidden Elo of other players).
Formula
component = clamp(50 + ((opponentAvgElo − 1500) ÷ 400)×50, 0, 100). Opponent avg = mean Elo of other players in the event.
Defaults
Weight 15% (opponent_strength_weight). Baseline 1500, spread 400.
Impact example
Opponents average 1700 → component 75. All at 1500 → 50 (neutral).
Code
lib/rankings/engine/performance-score.jsstrengthScore; opponent avg in recalculate.js

CR-06. Partner strength component

CR-06. Partner strength component

Purpose
Adjust blend when partner is stronger/weaker (doubles).
Formula
Same as CR-05 on partner’s Elo. No partner → treat as 1500 → component 50.
Defaults
Weight 10% (partner_strength_weight).
Impact example
Partner Elo 1300 → component 25 → pulls blended score down vs solo-neutral 50.
Code
lib/rankings/engine/performance-score.jsstrengthScore

CR-07. Age handicap component

CR-07. Age handicap component

Purpose
Optional senior boost in performance blend. Does not change hidden Elo.
Formula
Missing age → 50. Else clamp(50 + ((age − baseline) ÷ spread)×50, 0, 100).
Defaults
Weight 10% (age_weight). Baseline 50 years, spread 25 years. Set weight to 0 to disable.
Impact example
Age 75, baseline 50, spread 25 → component 100. If admin sets age weight to 10%, adds ~5 blend points vs age 50. Age 25 → component 0.
Code
lib/rankings/engine/performance-score.jsageHandicapScore, ageAtDate

CR-08. Blended performance score

CR-08. Blended performance score

Purpose
Single 0–100 session quality score (stored on result row; not the leaderboard headline).
Formula
Weighted average of six components. If sum of weights = 0 → 50.
Defaults
Global weights 40 / 20 / 15 / 15 / 10 / 10 (age). Series can override six blend weights when use_custom_weights is enabled.
Impact example
Components all 50 → blend 50 → adjustment 0. Win-heavy session: 75/100/100/50/50/50 at pickleball preset 40/22/18/12/8 → blend 80.00 (see CR-35).
Code
lib/rankings/engine/performance-score.jsblendPerformanceScore, computePerformanceScore

CR-09. Performance point adjustment (raw)

CR-09. Performance point adjustment (raw)

Purpose
Convert performance score into performance_points delta before weekly cap.
Formula
rawAdjustment = ((performanceScore − 50) ÷ 5) × effectiveEventWeight, rounded 2 decimals.
Defaults
effectiveEventWeight = event.weight × series.overall_weight_multiplier (each defaults 1.0).
Impact example
Score 60, weight 1.0+2.0 performance points. Score 80, weight 1.0+6.0. Score 500.
Code
lib/rankings/engine/performance-score.jsperformanceAdjustment

CR-10. Weekly gain cap

CR-10. Weekly gain cap

Purpose
Limit how much performance_points can rise per ISO week.
Formula
Week key YYYY-Www from event date. applied = min(rawDelta, max(0, maximumWeeklyGain − priorWeekTotal)) for positive deltas.
Defaults
maximum_weekly_gain = 15.
Impact example
Monday +10, Wednesday +10 raw → second applied only +5 (week total 15). Thursday +3 raw → 0 applied.
Code
lib/rankings/engine/weekly-clamp.jsclampWeeklyAdjustment

CR-11. Weekly loss cap

CR-11. Weekly loss cap

Purpose
Limit how much performance_points can fall per ISO week.
Formula
For negative deltas: applied = −min(|rawDelta|, max(0, maximumWeeklyLoss + priorWeekTotal)).
Defaults
maximum_weekly_loss = 10.
Impact example
Monday −8, Tuesday −8 raw → second applied −2 (week total −10).
Code
lib/rankings/engine/weekly-clamp.jsclampWeeklyAdjustment

CR-12. Expected Elo score

CR-12. Expected Elo score

Purpose
Win probability vs opponent for Elo update.
Formula
expected = 1 ÷ (1 + 10^((opponentElo − playerElo) ÷ 400)). Missing ratings → 1500.
Defaults
Fixed algorithm.
Impact example
1500 vs 1500 → expected 0.5 (coin flip).
Code
lib/rankings/engine/skill-rating.jsexpectedScore

CR-13. Elo K-factor

CR-13. Elo K-factor

Purpose
Smaller rating moves for experienced players.
Formula
Games 0–9 → K 32; 10–29 → 24; 30+ → 16.
Defaults
Fixed tiers.
Impact example
New player (0 games) gains +16 on equal win; veteran (30+ games) gains +8 on same outcome.
Code
lib/rankings/engine/skill-rating.jskFactor

CR-14. Elo rating update

CR-14. Elo rating update

Purpose
Hidden per-sport skill index after each event; drives CR-05/06 and club rating.
Formula
Session score from W/L/T (CR-15). delta = round(K × (score − expected), 2); newElo = round(old + delta, 2).
Defaults
Start 1500 (DEFAULT_SKILL_RATING). Not weekly-capped.
Impact example
1500 vs 1500 avg, win, K=32 → +16 → Elo 1516.0. Loss → −161484.0.
Code
lib/rankings/engine/skill-rating.jsupdateElo

CR-15. Session outcome score (Elo input)

CR-15. Session outcome score (Elo input)

Purpose
Collapse W/L/T into one 0–1 score for Elo.
Formula
No games → 0.5. Else (wins + ties×0.5) ÷ total.
Defaults
Win=1, tie=0.5, loss=0 implied.
Impact example
3W–1L → 0.75 (strong win session). All ties → 0.5.
Code
lib/rankings/engine/skill-rating.jssessionOutcomeScore

CR-16. Declared rating → Elo (seed)

CR-16. Declared rating → Elo (seed)

Purpose
Map self-reported 1.0–5.5 roster rating to internal Elo before first ranked game.
Formula
Anchor 3.5 ↔ 1500; ±0.5 skill ≈ ±200 Elo. elo = 1500 + (skill − 3.5)×400, clamp skill 1.0–5.5.
Defaults
Only when club_rating_enabled and first game.
Impact example
Self-reported 4.0 → seeds Elo 1700 before first recalc update.
Code
lib/rankings/club-rating-scale.jsmemberRatingToElo

CR-17. Elo → club rating (display)

CR-17. Elo → club rating (display)

Purpose
Member-visible 1.0–5.5 club rating from hidden Elo.
Formula
skill = 3.5 + (elo − 1500) ÷ 400, clamp 1.0–5.5, round 1 decimal.
Defaults
Requires club_rating_enabled.
Impact example
Elo 1516 → club rating 3.5 (display). Elo 17004.0.
Code
lib/rankings/club-rating-scale.jseloToClubRating

CR-18. Session club rating delta

CR-18. Session club rating delta

Purpose
Change in club rating after one event (session history).
Formula
round(nextClubRating − prevClubRating, 2); null if either missing.
Defaults
Stored on ranking_results.club_rating_delta.
Impact example
3.5 → 3.54 after strong session → delta +0.04 shown as “+0.04” in history.
Code
lib/rankings/club-rating-scale.jsclubRatingDelta

CR-19. Club rating vs declared delta (profile)

CR-19. Club rating vs declared delta (profile)

Purpose
“How far above/below self-reported?” on profile — not the session delta.
Formula
clubRating − declaredRating when feature on and events_played ≥ min_events_for_club_rating.
Defaults
club_rating_enabled false; min_events_for_club_rating 1. Display toggles gate profile / my ranking / leaderboard columns.
Impact example
Declared 3.5, club rating 3.8 after 2 events → shows +0.3 above self-report on profile. With feature off → member sees only declared rating.
Code
lib/rankings/club-rating-display.jsskillRatingToApi

CR-20. Participation bonus

CR-20. Participation bonus

Purpose
Flat points per event appearance.
Formula
Each completed result row: participation_points += participation_bonus.
Defaults
0 globally; pickleball preset 1.
Impact example
Bonus 1, 5 events → +5.0 participation_points → total rank rises even with neutral performance sessions.
Code
lib/rankings/engine/recalculate.js; participation_bonus in settings

CR-21. Champion bonus

CR-21. Champion bonus

Purpose
Flat bonus for 1st-place finish.
Formula
When placement === 1: performance_points += champion_bonus.
Defaults
0 globally; pickleball preset 3.
Impact example
Champion bonus 3 on 1st place adds +3.0 performance on top of performance adjustment (CR-09).
Code
lib/rankings/engine/recalculate.js

CR-22. Volunteer bonus

CR-22. Volunteer bonus

Purpose
Points for volunteering at sessions (admin “Vol” flag).
Formula
One bonus row per volunteer session: volunteer_points = sum(volunteer_bonus).
Defaults
volunteer_bonus 5.
Impact example
3 volunteer sessions → 15.0 volunteer_points. Can move player several ranks without winning.
Code
lib/rankings/volunteers.js, lib/rankings/bonuses.js

CR-23. Fast-confirm bonus

CR-23. Fast-confirm bonus

Purpose
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.jstryFastConfirmBonus

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.
Code
lib/rankings/inactivity.jsapplyInactivityDecay

CR-25. Total ranking points

CR-25. Total ranking points

Purpose
Leaderboard sort key and public “total points”.
Formula
performance + participation + volunteer + sportsmanship (each rounded 2 decimals).
Defaults
N/A.
Impact example
12.5 + 3 + 10 + 1 = 26.5 total. Player with higher total ranks above on leaderboard.
Code
lib/rankings/constants.jstotalPoints

CR-26. Leaderboard eligibility

CR-26. Leaderboard eligibility

Purpose
Hide new players until enough events.
Formula
List only profiles where events_played ≥ min_events_for_leaderboard; sort by total points descending.
Defaults
min_events_for_leaderboard 3.
Impact example
Player with 2 events and 20 points computed → not listed on leaderboard until 3rd event.
Code
lib/routes/rankings/leaderboard.js

CR-27. Rolling window start

CR-27. Rolling window start

Purpose
“Last N months” leaderboard replay window.
Formula
rollingStart = today − rolling_months calendar months.
Defaults
rolling_months 12.
Impact example
Event 13 months ago counts on all-time but excluded from rolling leaderboard — player may rank higher on rolling than all-time.
Code
lib/rankings/engine/recalculate.jsrollingMonthStart

CR-28. Series effective event weight

CR-28. Series effective event weight

Purpose
Mini-league events can count more/less toward club totals.
Formula
effectiveWeight = event.weight_multiplier × series.overall_weight_multiplier.
Defaults
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.jsbuildClubDashboard

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.
Defaults
Feeds same engine as CSV/manual entry. Tiebreaker uses section or mini-league custom weights.
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.
Code
lib/rankings/club-play/aggregate.jsaggregateGamesToResults; lib/rankings/club-play/session-placement.jsassignSessionPlacements

CR-30b. Session placement tiebreaker

CR-30b. Session placement tiebreaker

Purpose
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 mini-league 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.
Code
lib/rankings/engine/performance-score.jscomputeTiebreakScore; lib/rankings/club-play/session-placement.jsassignSessionPlacements

CR-31. Stored Glicko RD

CR-31. Stored Glicko RD

Purpose
Reserved field on skill rating row.
Formula
Not recalculated in current implementation; stored constant 350 (DEFAULT_SKILL_RD).
Defaults
350.
Impact example
No effect on leaderboard or Elo today.
Code
lib/rankings/constants.js

CR-32. Win/loss streak

CR-32. Win/loss streak

Purpose
Profile stat and achievements (“Hot streak”).
Formula
Session “won” if wins > losses. Streak increments on consecutive wins, negative on losses; longest_streak tracks max |streak|.
Defaults
N/A.
Impact example
Three winning sessions → streak 3; may unlock achievement; does not add points.
Code
lib/rankings/engine/recalculate.jsupdateStreak

CR-33. Achievement thresholds

CR-33. Achievement thresholds

Purpose
Badges when profile crosses rules.
Formula
Rules compare events_played, championships, longest_streak, total_points, etc. to thresholds.
Defaults
e.g. “Century club” at 100+ total points.
Impact example
Total crosses 100 → badge awarded; no point change.
Code
lib/rankings/achievements.jsprofileMeetsRule

CR-34. Leaderboard visibility

CR-34. Leaderboard visibility

Purpose
Who can view leaderboard API.
Formula
Not numeric — members_only requires sign-in; public allows anonymous read.
Defaults
members_only.
Impact example
Public setting lets marketing page show top 10 without login.
Code
lib/routes/rankings/leaderboard.js

CR-35. Worked example — full session

CR-35. Worked example — full session

Purpose
End-to-end illustration (Player A, pickleball preset, first event of week).
Formula
Defaults
Impact example
Player A jumps to rank 1 on a small leaderboard after one strong winning session.
Code
See docs/CLUB-RANKINGS-SPECIFICATIONS.md §16
StepResult
ComponentsWin 75, placement 100, pt diff 100, opp 50, partner 50
Blend (40/22/18/12/8)80.00
Raw adjustment+6.0
Weekly cap+6.0 applied
Participation+1.0
Champion+3.0 performance
Total points10.0
Elo1516.0
Club rating (if enabled)~3.5+ from 1516 Elo

Part 2 — CCM Core

*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. Legacy ids starter / growth / pro map to CCM economics.
Impact example
CCM list $40; tenant custom $35 → checkout charges $35/mo.
Code
lib/pricing.jseffectivePricing; lib/billing-plans.js

CCM-02. Overage count (members or courts)

CCM-02. Overage count (members or courts)

Purpose
Units above plan inclusion that drive extra monthly charge.
Formula
Defaults
Rankings-only 150 members; CCM 6 courts included, unlimited members (memberLimit null).
Impact example
8 active courts on CCM → 2 overage courts. 105 active on Rankings → 5 overage members.
Code
lib/billing-overage.jsoverageMembers, overageCourts

CCM-03. Overage charge

CCM-03. Overage charge

Purpose
Extra monthly amount on billing estimate (and Rankings invoice renewal for member overage).
Formula
Defaults
CCM $5/extra court; Rankings $0.40/extra member. Skipped when custom pricing flag set.
Impact example
2 extra courts on CCM → $10/mo. 5 overage on Rankings → $2.00/mo.
Code
lib/billing-overage.jsoverageChargeForPlan

CCM-04. Estimated monthly total

CCM-04. Estimated monthly total

Purpose
Admin billing dashboard total.
Formula
round((baseMonthly + overageCharge) × 100) / 100.
Defaults
Skips overage if custom pricing flag set. CCM court overage uses live court count.
Impact example
CCM $40 + 2 courts × $5 → $50 estimated.
Code
lib/billing-overage.jsestimatedMonthlyTotal

CCM-05. Upgrade nudge

CCM-05. Upgrade nudge

Purpose
Suggest full CCM when Rankings-only overage makes the next tier a better fit.
Formula
If estimatedTotal ≥ nextPlan.priceMonthly → show nudge to next self-serve plan.
Defaults
Self-serve upgrade chain: rankings → ccm only (no Starter/Growth/Pro ladder).
Impact example
Rankings estimate ~$45/mo with member overage → nudge to Club Court Manager ($40 base + court add-ons).
Code
lib/billing-overage.jsupgradeNudge

CCM-06. Trial days remaining

CCM-06. Trial days remaining

Purpose
Billing UI countdown.
Formula
ceil((trial_ends_at − now) ÷ 86400000) days.
Defaults
TRIAL_DAYS 14 on onboard.
Impact example
12 days left shown; at 0, subscription must be active to avoid trial expiry flows.
Code
lib/billing.js

CCM-07. Active member count (billable)

CCM-07. Active member count (billable)

Purpose
Rankings member overage and analytics. CCM self-serve is not capped by member count.
Formula
SQL count: status = active, activated_at IS NOT NULL, not deactivated.
Defaults
Imported roster awaiting invite not counted. CCM plan memberLimit is null (unlimited).
Impact example
98 activated + 10 pending invite → count 98 for Rankings billing / analytics.
Code
lib/billing.js

CCM-08. Booking slot alignment

CCM-08. Booking slot alignment

Purpose
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.jsvalidateBooking

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 60rejected.
Code
lib/rules.jsvalidateBooking

CCM-10. Advance booking window

CCM-10. Advance booking window

Purpose
How far ahead members may book.
Formula
dayDiff(todayClubDate, bookingDate) ≤ advance_days.
Defaults
Often 7 days.
Impact example
7-day advance: booking 8 days out → rejected; day 7 → allowed.
Code
lib/rules.jsvalidateBooking

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/demo.js for demo override

CCM-12. Daily reservation limit

CCM-12. Daily reservation limit

Purpose
Max bookings per member per sport per day.
Formula
Count reservations where member is booker or accepted invitee; block if count ≥ max_per_day.
Defaults
Section max_per_day often 1–2.
Impact example
max_per_day = 1, second booking same day → rejected with daily limit message.
Code
lib/daily-limit.js

CCM-13. Cancellation notice

CCM-13. Cancellation notice

Purpose
Require notice before canceling.
Formula
If minutesSince(created) ≤ 30 → grace (allow). Else minutesUntilStart ≥ min_cancel_min (default 120).
Defaults
30-min grace; 120-min notice.
Impact example
Cancel 1 hour before start with 2-hour rule → blocked.
Code
lib/rules.jsvalidateCancellation

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.
Defaults
Open/close from section (e.g. 7:00–21:00).
Impact example
7:00–21:00 @ 30 min → 28 slot rows per court.
Code
lib/routes/sections.js; client public/js/court-grid.js

CCM-15. Grid occupancy paint

CCM-15. Grid occupancy paint

Purpose
Show booked cells.
Formula
For each reservation, mark slots [start, end) stepping incrementMin.
Defaults
N/A.
Impact example
10:00–11:00 booking → two 30-min cells show as occupied.
Code
public/js/court-grid.js

CCM-16. Count bookable slots

CCM-16. Count bookable slots

Purpose
Max contiguous length member can book from a start time.
Formula
Walk forward until occupied, past close, or (slots+1)×increment > max_length_min.
Defaults
Uses section max length.
Impact example
Start 10:00, block at 10:30, max 60 min → only 1 slot (30 min) bookable.
Code
public/js/court-grid.jscountBookableSlots

CCM-17. Court group skill eligibility

CCM-17. Court group skill eligibility

Purpose
Restrict courts by self-reported skill band.
Formula
ratingInRange(memberRating, skill_min, skill_max) — boolean.
Defaults
Group labels like “3.0–4.0” are display only.
Impact example
Member 4.5 on 3.0–4.0 court group → cannot book those courts.
Code
lib/booking-eligibility.js, lib/rating.js

CCM-18. Member rating parse

CCM-18. Member rating parse

Purpose
Validate roster and court group ratings.
Formula
Member rating clamp 1.0–5.5, round 2 decimals; court group range 0–8, round 1 decimal.
Defaults
N/A.
Impact example
Enter 6.0 on roster → rejected; 3.25 stored as 3.25.
Code
lib/rating.jsparseMemberRating

CCM-19. Event registration cap

CCM-19. Event registration cap

Purpose
Close registration when full.
Formula
count(status in registered, walk_in) ≥ max_participants → reject new registration.
Defaults
Per event max_participants.
Impact example
20 cap, 20 registered → next signup sees event full.
Code
lib/routes/events/register.js, lib/club-events.js

CCM-20. Invitation players needed

CCM-20. Invitation players needed

Purpose
Lineup confirmation target.
Formula
playersNeeded = max(1, invitees + 1); acceptedCount = 1 + acceptedInvitees.
Defaults
N/A.
Impact example
3 invitees → need 4 confirmed (booker + 3).
Code
lib/routes/reservations/invitations.js

CCM-21. Lineup progress percent

CCM-21. Lineup progress percent

Purpose
Progress bar on reservation UI.
Formula
min(100, round(accepted ÷ needed × 100)).
Defaults
N/A.
Impact example
3 of 4 accepted → 75% progress.
Code
public/js/player-lineup.js

CCM-22. Reminder target date

CCM-22. Reminder target date

Purpose
Which reservations get tomorrow’s reminder email/SMS.
Formula
Per tenant TZ: tomorrow = addDays(clubToday, 1); cron selects res_date = tomorrow.
Defaults
Daily cron ~1 PM UTC (see vercel.json).
Impact example
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).
Code
lib/billing-invoice-overage.js, lib/billing-overage.jsoverageInvoiceEligible

Part 3 — Club Play scheduling

*These affect pairings and court rotation, not ranking points.*

CP-01. Repeat-minimization score

CP-01. Repeat-minimization score

Purpose
Pick random-mix round with fewest repeat partners/opponents.
Formula
score = sum(partnerPairCounts) + sum(opponentPairCounts) across round matchups; try up to 100 shuffles, pick lowest (0 = no repeats).
Defaults
N/A.
Impact example
Score 0 vs 4 → algorithm prefers assignment with 0 (fresh pairings).
Code
lib/rankings/club-play/pairing.jsscoreRoundAssignment

CP-02. Ladder seeding order

CP-02. Ladder seeding order

Purpose
Initial ladder positions from mini-league standings.
Formula
Sort teams by totalPoints DESC, then signup order for unranked.
Defaults
Uses CR total points from same series.
Impact example
Standings 103:50, 101:40, 102:30 → ladder order [103, 101, 102, …].
Code
lib/rankings/club-play/pairing.jsseedLadderOrder

CP-03. Ladder upset swap

CP-03. Ladder upset swap

Purpose
Promote winner when lower rank beats higher rank.
Formula
If higher-index (better rank) team loses, swap winner and loser positions in ladderOrder.
Defaults
N/A.
Impact example
#101 loses to #102 → order flips → 102 now above 101 → next round matchups change.
Optional length
Session target_rounds stops auto-advance after that round (tryAutoAdvanceRoundmax_rounds). Blank = unlimited until End session.
Code
lib/rankings/club-play/pairing.jsladderNextRound; lib/rankings/club-play/round-flow.jstryAutoAdvanceRound

CP-04. King of the Court rotation

CP-04. King of the Court rotation

Purpose
Winner stays on court; challengers rotate.
Formula
Qualitative rotation — winner remains, losers move to queue/other courts; ties keep both teams.
Defaults
N/A.
Impact example
Court 1 winner plays new challengers from queue each round.
Optional length
Session target_rounds stops auto-advance after that round (tryAutoAdvanceRoundmax_rounds).
Code
lib/rankings/club-play/pairing.jskotcNextRound; lib/rankings/club-play/round-flow.jstryAutoAdvanceRound

CP-05. Round-robin schedule

CP-05. Round-robin schedule

Purpose
Fixed rotation of partnerships/opponents across rounds.
Formula
Precomputed schedule indices — no numeric scoring.
Defaults
N/A.
Impact example
8 players get 7 rounds with varied partners per schedule table.
Optional length
Pass targetRounds to schedule exactly N rounds; when omitted, default is ceil(teams / 2) (or ceil(players / 2) for random-mix doubles).
Code
lib/rankings/club-play/pairing.jsroundRobinSchedule / randomMixSchedule

CP-06. Swiss next-round pairing

CP-06. Swiss next-round pairing

Purpose
Each round, pair players by current session record so similar standings meet; fill all courts.
Formula
Rank active players by session wins DESC, then point differential DESC (same tiebreaks as session placement). Form doubles courts from nearby ranks (via buildTeams). Prefer assignments with lower scoreRoundAssignment (fewer repeat partners/opponents). 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).
Code
lib/rankings/club-play/pairing.jsswissNextRound / sessionPlayerStandings; lib/rankings/adapters/swiss.js; lib/rankings/club-play/round-flow.jstryAutoAdvanceRound

Part 4 — Appendices

A. Master defaults (selected)

Pickleball first-time preset: weights 40/22/18/12/8, champion 3, participation 1, min events 3 (lib/rankings/sport-templates.js).

DomainSettingDefault
CRPerformance weights40 / 20 / 15 / 15 / 10 / 10
CRWeekly max gain / loss15 / 10
CRMin events (leaderboard)3
CRMin events (club rating show)1
CRRolling months12
CRClub rating featureoff
CRVolunteer bonus5
BillingCCM$40/mo · 6 courts included · +$5/court · unlimited members
BillingRankings$19/mo · 150 members · $0.40/member overage
BillingEnterpriseCustom (contact sales)
BillingTrial14 days
MembershipPlatform feeDefault 0% (STRIPE_CONNECT_APPLICATION_FEE_PERCENT)

B. Glossary

TermMeaning
performance_pointsPublic bucket from session quality + champion bonus
participation_pointsFlat per-event attendance
volunteer_pointsFrom Vol flags
sportsmanship_pointsFast-confirm and similar
Total pointsSum of four buckets — leaderboard sort
Hidden EloInternal ~1500 scale; not shown to members
Club rating1.0–5.5 display derived from Elo when enabled
Declared ratingSelf-reported roster skill (1.0–5.5)
Performance score0–100 blend per session (audit, not headline)

C. Code index

FileCalculations
lib/rankings/engine/performance-score.jsCR-02–09, CR-07, CR-30b
lib/rankings/engine/skill-rating.jsCR-12–15
lib/rankings/engine/weekly-clamp.jsCR-10–11
lib/rankings/engine/recalculate.jsCR-01, CR-20–21, CR-25, CR-27–28, CR-32
lib/rankings/club-rating-scale.jsCR-16–18
lib/rankings/club-rating-display.jsCR-19
lib/billing-overage.jsCCM-02–05, CCM-23
lib/rules.jsCCM-08–11, CCM-13
lib/daily-limit.jsCCM-12
public/js/court-grid.jsCCM-15–16
lib/rankings/club-play/pairing.jsCP-01–06
lib/rankings/club-play/aggregate.jsCR-30
lib/rankings/club-play/session-placement.jsCR-30, CR-30b

---

*Club Court Manager™ powered by StoufferAI*

DocumentPurpose
CLUB-RANKINGS-SPECIFICATIONS.mdCR formulas deep dive
public/docs/ccm-user-manual.htmlCCM workflows
public/docs/club-rankings-user-manual.htmlCR workflows
DESKTOP-TEST-MANUAL.mdQA script