Skip to main content

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

FormatAllowance
Individual stroke/stableford95% or 100%
Four-ball better ball85% or 90%
Foursomes50% of combined
Greensomes60% 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

PCCMeaning
-1Easier conditions (scores lower than expected)
0Normal conditions
+1Slightly harder
+2Harder
+3Significantly harder conditions

Source priority

PCC is resolved in this order:

  1. Stored - Previously calculated and persisted in DailyCourseConditions
  2. Association - Fetched from GolfRSA/DotGolf API
  3. Manual - Set by competition admin on the round
  4. Computed - Calculated from field performance (requires ≥8 scorecards)

Computation method

When computed locally:

  1. Collect NDB-adjusted scores from all valid scorecards
  2. Calculate expected score for each player (SSS + Course Handicap)
  3. Compute nett differentials (Actual - Expected)
  4. 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

  1. Player enters competition
  2. Resolve handicap provider (from club region or explicit)
  3. Fetch current handicap from provider API
  4. 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

ModeBehavior
AUTOPost automatically on finalization
MANUALAdmin triggers posting
NONEPosting 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);
}