Handicap Integration
WHS (World Handicap System) compliant handicap calculations, Playing Conditions Calculation (PCC), Competition Standard Scratch (CSS), and handicap posting to associations.
Course handicap
Formula
Course Handicap = (Handicap Index × Slope Rating / 113) + (Course Rating - Par)
Implementation
function calculateCourseHandicap(params: {
handicapIndex: number;
slopeRating: number;
courseRating: number;
par: number;
}): number {
const { handicapIndex, slopeRating, courseRating, par } = params;
const ch = (handicapIndex * slopeRating / 113) + (courseRating - par);
return Math.round(ch); // Round to nearest whole number
}
Example
Handicap Index: 15.3
Course Rating: 72.4
Slope Rating: 131
Par: 72
Course Handicap = (15.3 × 131 / 113) + (72.4 - 72)
= 17.74 + 0.4
= 18.14
≈ 18
Playing handicap
Playing handicap applies the competition's handicap allowance:
Playing Handicap = Course Handicap × Handicap Allowance %
Common allowances
| Format | Allowance |
|---|---|
| Individual stroke/stableford | 95% or 100% |
| Four-ball better ball | 85% or 90% |
| Foursomes | 50% of combined |
| Greensomes | 60% low + 40% high |
| Scramble (4-person) | 10% of combined |
Playing Conditions Calculation (PCC)
PCC adjusts scores based on abnormal playing conditions (weather, course setup).
Range
| PCC | Meaning |
|---|---|
| -1 | Easier conditions (scores lower than expected) |
| 0 | Normal conditions |
| +1 | Slightly harder |
| +2 | Harder |
| +3 | Significantly harder conditions |
Source priority
PCC is resolved in this order:
- Stored - Previously calculated and persisted in
DailyCourseConditions - Association - Fetched from GolfRSA/DotGolf API
- Manual - Set by competition admin on the round
- Computed - Calculated from field performance (requires ≥8 scorecards)
Computation method
When computed locally:
- Collect NDB-adjusted scores from all valid scorecards
- Calculate expected score for each player (SSS + Course Handicap)
- Compute nett differentials (Actual - Expected)
- Apply statistical model to determine PCC
interface PccRoundInput {
playerId: string;
handicapIndex: number;
adjustedGrossScore: number; // NDB-adjusted
courseRating: number;
slopeRating: number;
holesPlayed: 9 | 18;
}
function computePcc(inputs: PccRoundInput[]): PccValue {
// Requires minimum 8 valid scorecards
// Returns -1, 0, 1, 2, or 3
}
Data storage
// DailyCourseConditions table
{
courseId: string;
courseLayoutId: string | null;
playedOn: Date; // Date only (no time)
pcc: number; // -1 to 3
source: 'STORED' | 'ASSOCIATION' | 'MANUAL' | 'COMPUTED';
}
Round configuration
{
roundNumber: 1,
date: new Date('2025-12-15'),
courseId: 'course-123',
courseRating: 72.4,
slopeRating: 131,
pcc: 1, // Manual PCC override
isHandicapQualifying: true,
}
Competition Standard Scratch (CSS)
CSS represents the expected scratch score for the day, adjusted for field performance.
Calculation
CSS = SSS + Adjustment
Where adjustment is based on:
- Field size (minimum required)
- Nett differentials of all completed scorecards
- Buffer zone thresholds
Implementation
interface CssInput {
sss: number; // Standard Scratch Score (Course Rating)
nettDifferentials: number[]; // (Gross - Expected) for each player
minScores: number; // Minimum field size (default: 10)
}
function calculateCss(input: CssInput): { css: number } {
// Returns SSS if field too small
// Otherwise applies buffer adjustment
}
Nett differential
Nett Differential = Gross Score - Expected Score
Expected Score = SSS + Course Handicap
Handicap snapshot
At entry time, capture the player's current handicap:
interface HandicapSnapshot {
id: string;
entryId: string;
handicapIndex: number;
courseHandicap: number;
playingHandicap: number;
provider: string; // 'GOLFRSA', 'DOTGOLF', 'MANUAL'
membershipNumber: string | null;
capturedAt: Date;
}
Capture flow
- Player enters competition
- Resolve handicap provider (from club region or explicit)
- Fetch current handicap from provider API
- Store snapshot linked to entry
await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-456',
handicapProvider: 'GOLFRSA',
membershipNumber: 'RSA123456',
});
// Handicap snapshot created automatically
Manual override
For guests or players without provider membership:
await entriesService.create({
competitionId: 'comp-123',
playerId: 'guest-789',
handicapIndexOverride: 18.5, // Manual handicap
});
Handicap posting
Post qualifying scores to associations after competition completion.
Posting modes
| Mode | Behavior |
|---|---|
AUTO | Post automatically on finalization |
MANUAL | Admin triggers posting |
NONE | Posting disabled |
Requirements
- Round must be
isHandicapQualifying: true - Round must not have
isWinterRulesApplied: true - Player must have valid membership number
- Provider credentials must be configured
Posted data
interface HandicapPostingPayload {
membershipNumber: string;
courseId: string;
date: Date;
grossScore: number;
adjustedGrossScore: number; // NDB-adjusted
courseRating: number;
slopeRating: number;
pcc: number;
holesPlayed: 9 | 18;
}
Provider configuration
// Competition
{
handicapPostingMode: 'AUTO',
}
// Round
{
isHandicapQualifying: true,
isWinterRulesApplied: false,
}
// Environment
GOLFRSA_API_KEY=xxx
GOLFRSA_API_SECRET=xxx
DOTGOLF_API_KEY=xxx
Winter rules
When winter rules (preferred lies) are in effect:
{
roundNumber: 1,
date: new Date('2025-01-15'),
courseId: 'course-123',
isWinterRulesApplied: true,
winterNotes: 'Preferred lies through the green',
}
Effects:
- Round is not handicap qualifying
- PCC not calculated
- Scores not posted to associations
- CSS defaults to SSS
Qualifying checklist
Track handicap qualifying compliance:
{
isHandicapQualifying: true,
qualifyingChecklistCompleted: true,
qualifyingNotes: 'All markers verified, cards attested',
}
Checklist items (informational):
- Proper course setup
- Markers playing from correct tees
- Cards properly marked and attested
- Competition conditions met
Score differential
For handicap index calculation (posted to associations):
Score Differential = (113 / Slope Rating) × (Adjusted Gross - Course Rating - PCC)
function calculateScoreDifferential(params: {
adjustedGross: number;
courseRating: number;
slopeRating: number;
pcc: number;
}): number {
const { adjustedGross, courseRating, slopeRating, pcc } = params;
return (113 / slopeRating) * (adjustedGross - courseRating - pcc);
}