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
- All scorecards locked
- Final positions calculated with countback
- Division positions calculated
CompetitionResultrecords created- Competition status →
COMPLETED RESULTS_FINALIZEDevent published- 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
| Status | Description |
|---|---|
OPEN | Appeal submitted |
UNDER_REVIEW | Being investigated |
UPHELD | Appeal accepted, changes made |
REJECTED | Appeal denied |
WITHDRAWN | Appellant 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
| Method | Endpoint | Description |
|---|---|---|
GET | /competitions/:id/leaderboard | Live leaderboard |
GET | /competitions/:id/leaderboard.csv | CSV export |
GET | /competitions/:id/stats | Competition statistics |
POST | /competitions/:id/finalize | Finalize results |
GET | /players/:id/results | Player result history |
POST | /competitions/:id/prizes | Assign prizes |
POST | /competitions/:id/appeals | Open appeal |
PATCH | /competitions/:id/appeals/:appealId | Update appeal |