Skip to main content

Results & Exports

Leaderboards, result finalization, CSV exports, prizes, appeals, and domain events.

Leaderboard

Get live leaderboard

const leaderboard = await resultsService.getLeaderboard({
competitionId: 'comp-123',
});

With options

const leaderboard = await resultsService.getLeaderboard({
competitionId: 'comp-123',
divisionId: 'div-a', // Filter by division
includeWithdrawn: false, // Exclude withdrawn (default)
});

Response

interface LeaderboardResult {
competitionId: string;
competitionName: string;
format: CompetitionFormat;
isFinalized: boolean;
lastUpdated: Date;
qualifying: boolean; // Is handicap qualifying
winterRulesApplied: boolean;
css: number | null; // Competition Standard Scratch
pcc: number | null; // Playing Conditions Calc
pccByRound: RoundPcc[];
rows: LeaderboardRow[];
}

interface LeaderboardRow {
position: number;
isTied: boolean;
entryId: string;
playerId: string;
playerName: string;
handicap: number;
divisionId: string | null;
divisionName: string | null;
grossTotal: number | null;
netTotal: number | null;
stablefordPoints: number | null;
toPar: number | null;
thru: number | 'F'; // Holes completed or 'F' for finished
holesRemaining: number;
roundScores: RoundScore[];
countback: CountbackScores | null;
}

Example response

{
"competitionId": "comp-123",
"competitionName": "Monthly Medal",
"format": "STABLEFORD",
"isFinalized": false,
"lastUpdated": "2025-12-15T14:30:00Z",
"qualifying": true,
"css": 72.3,
"pcc": 1,
"rows": [
{
"position": 1,
"isTied": false,
"entryId": "entry-a",
"playerName": "John Smith",
"handicap": 12.4,
"stablefordPoints": 38,
"thru": "F",
"countback": { "back9": 20, "back6": 14, "back3": 8, "last": 3 }
},
{
"position": 2,
"isTied": true,
"entryId": "entry-b",
"playerName": "Jane Doe",
"handicap": 8.2,
"stablefordPoints": 36,
"thru": "F"
},
{
"position": 2,
"isTied": true,
"entryId": "entry-c",
"playerName": "Bob Wilson",
"handicap": 15.1,
"stablefordPoints": 36,
"thru": 16
}
]
}

Finalization

Finalize results

await resultsService.finalizeResults({
competitionId: 'comp-123',
});

What happens on finalization

  1. All scorecards locked
  2. Final positions calculated with countback
  3. Division positions calculated
  4. CompetitionResult records created
  5. Competition status → COMPLETED
  6. RESULTS_FINALIZED event published
  7. Handicap posting triggered (if AUTO)

Pre-finalization checks

  • All rounds completed
  • All scorecards attested or locked
  • No pending appeals

Competition statistics

const stats = await resultsService.getStats('comp-123');

Response

interface CompetitionStats {
competitionId: string;
entriesTotal: number;
entriesWithdrawn: number;
entriesCompleted: number;
averageGross: number | null;
averageNet: number | null;
averageStableford: number | null;
lowestGross: number | null;
lowestNet: number | null;
highestStableford: number | null;
css: number | null;
pcc: number | null;
pccByRound: RoundPcc[];
}

Player results history

const history = await resultsService.getPlayerResults('player-456', {
clubId: 'club-123',
seasonId: 'season-2025',
limit: 20,
});

Response

[
{
competitionId: 'comp-123',
competitionName: 'December Medal',
date: '2025-12-15',
format: 'STABLEFORD',
overallPosition: 3,
divisionPosition: 1,
divisionName: 'B Division',
totalStableford: 36,
isTied: false
},
// ...
]

CSV export

Export leaderboard

const csv = await resultsExportService.exportLeaderboard('comp-123');

Endpoint

GET /api/competitions/:id/leaderboard.csv

Output format

Position,Tied,Player,Handicap,Division,R1,R2,Total,To Par
1,N,John Smith,12.4,A Division,38,36,74,-2
T2,Y,Jane Doe,8.2,A Division,35,37,72,E
T2,Y,Bob Wilson,15.1,B Division,36,36,72,E

Prizes

Assign prizes

await prizeService.assignPrize({
competitionId: 'comp-123',
entryId: 'entry-a',
prizeDescription: '1st Place Overall',
prizeValue: 50000, // R500.00 in cents
});

Prize data

interface PrizeAssignment {
competitionId: string;
entryId: string;
prizeDescription: string;
prizeValue: number | null; // In cents
}

Common prize structures

// Overall prizes
await prizeService.assignPrizes('comp-123', [
{ entryId: 'entry-1', prizeDescription: '1st Overall', prizeValue: 50000 },
{ entryId: 'entry-2', prizeDescription: '2nd Overall', prizeValue: 30000 },
{ entryId: 'entry-3', prizeDescription: '3rd Overall', prizeValue: 20000 },
]);

// Division prizes
await prizeService.assignPrizes('comp-123', [
{ entryId: 'entry-5', prizeDescription: 'A Division Winner', prizeValue: 25000 },
{ entryId: 'entry-8', prizeDescription: 'B Division Winner', prizeValue: 25000 },
]);

// Special prizes
await prizeService.assignPrizes('comp-123', [
{ entryId: 'entry-12', prizeDescription: 'Nearest Pin Hole 7', prizeValue: 10000 },
{ entryId: 'entry-15', prizeDescription: 'Longest Drive', prizeValue: 10000 },
]);

Appeals

Open appeal

await appealsService.openAppeal({
competitionId: 'comp-123',
entryId: 'entry-456',
reason: 'Incorrect score recorded on hole 12',
requestedBy: 'player-789',
});

Update appeal

await appealsService.updateAppeal({
appealId: 'appeal-123',
status: 'UNDER_REVIEW',
notes: 'Reviewing marker scorecard',
});

Resolve appeal

await appealsService.resolveAppeal({
appealId: 'appeal-123',
resolution: 'UPHELD',
notes: 'Score corrected from 6 to 5 on hole 12',
adjustedScore: { hole12: 5 },
});

Appeal statuses

StatusDescription
OPENAppeal submitted
UNDER_REVIEWBeing investigated
UPHELDAppeal accepted, changes made
REJECTEDAppeal denied
WITHDRAWNAppellant withdrew

Domain events

RESULTS_FINALIZED

Published when competition results are finalized:

interface ResultsFinalizedPayload {
competitionId: string;
competitionName: string;
clubId?: string;
format: CompetitionFormat;
totalEntries: number;
timestamp: string;
winners: Array<{
position: number;
playerId: string;
playerName: string;
score: number;
prizeDescription?: string;
prizeValue?: number;
playerEmail?: string | null;
playerPhone?: string | null;
}>;
}

Event consumers

  • Notifications: Email/SMS results to players
  • Handicap posting: Trigger score posting to associations
  • Analytics: Update club statistics
  • Series: Update Order of Merit standings

Series / Order of Merit

Aggregate results across multiple competitions.

Create series

await seriesService.create({
clubId: 'club-123',
name: 'Order of Merit 2025',
seasonId: 'season-2025',
scoringSystem: 'POINTS', // or 'POSITIONS'
competitionIds: ['comp-1', 'comp-2', 'comp-3'],
});

Get standings

const standings = await seriesService.getStandings('series-123');

Response

[
{
position: 1,
playerId: 'player-a',
playerName: 'John Smith',
points: 125,
eventsPlayed: 8,
bestFinishes: [1, 2, 3],
},
// ...
]

API endpoints

MethodEndpointDescription
GET/competitions/:id/leaderboardLive leaderboard
GET/competitions/:id/leaderboard.csvCSV export
GET/competitions/:id/statsCompetition statistics
POST/competitions/:id/finalizeFinalize results
GET/players/:id/resultsPlayer result history
POST/competitions/:id/prizesAssign prizes
POST/competitions/:id/appealsOpen appeal
PATCH/competitions/:id/appeals/:appealIdUpdate appeal