Skip to main content

Entries & Eligibility

Player entries, divisions, eligibility rules, team events, and withdrawal management.

Entry workflow

┌──────────┐    ┌────────────┐    ┌───────────┐    ┌───────────┐
│ OPEN │───▶│ ENTERED │───▶│ PLAYING │───▶│ COMPLETED │
│ (window) │ │ (snapshot) │ │ (scoring) │ │ (results) │
└──────────┘ └────────────┘ └───────────┘ └───────────┘


┌───────────┐
│ WITHDRAWN │
└───────────┘

Creating entries

Basic entry

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-456',
});

Entry with division

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-456',
divisionId: 'div-a', // A Division
});

Entry with handicap provider

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-456',
handicapProvider: 'GOLFRSA',
membershipNumber: 'RSA123456',
});

Guest entry (manual handicap)

await entriesService.create({
competitionId: 'comp-123',
playerId: 'guest-789',
handicapIndexOverride: 18.5,
});

Entry linked to tee time

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-456',
teeTimeSlotId: 'slot-abc', // Existing booking
});

Entry data

interface CompetitionEntry {
id: string;
competitionId: string;
playerId: string;
divisionId: string | null;
teeTimeSlotId: string | null;
enteredAt: Date;
withdrawnAt: Date | null;
withdrawalReason: string | null;

// Relationships
player: Player;
division: CompetitionDivision | null;
handicapSnapshot: HandicapSnapshot | null;
scorecards: Scorecard[];
teamMembers: CompetitionTeamMember[];
}

Divisions

Divisions segment players by handicap, gender, or age.

Creating divisions

await competitionsService.create({
// ...competition details
divisions: [
{
name: 'A Division',
sortOrder: 1,
maxHandicap: 12,
},
{
name: 'B Division',
sortOrder: 2,
minHandicap: 13,
maxHandicap: 24,
},
{
name: 'C Division',
sortOrder: 3,
minHandicap: 25,
},
{
name: 'Ladies',
sortOrder: 4,
gender: 'F',
},
{
name: 'Seniors',
sortOrder: 5,
minAge: 55,
},
],
});

Division configuration

interface CreateDivisionInput {
name: string;
sortOrder?: number; // Display order
minHandicap?: number; // Minimum handicap index
maxHandicap?: number; // Maximum handicap index
gender?: 'M' | 'F'; // Gender restriction
minAge?: number; // Minimum age
maxAge?: number; // Maximum age
teeSet?: number; // Tee set for this division
}

Division auto-assignment

When divisionId is not provided, the system can auto-assign based on:

  1. Player's handicap index (from snapshot)
  2. Player's gender (from profile)
  3. Player's age (calculated from DOB)

Eligibility rules

Competition-level eligibility

interface CompetitionEligibilityConfig {
minHandicap?: number;
maxHandicap?: number;
allowedGenders?: Array<'M' | 'F'>;
minAge?: number;
maxAge?: number;
requireHomeClub?: boolean; // Must be member of hosting club
allowGuests?: boolean; // Allow non-members
allowedMembershipStatuses?: string[]; // e.g., ['FULL', 'ASSOCIATE']
allowedAssociationProviders?: string[]; // e.g., ['GOLFRSA', 'DOTGOLF']
}

Usage

await competitionsService.create({
// ...
eligibility: {
maxHandicap: 24,
allowedGenders: ['M'],
minAge: 18,
requireHomeClub: true,
allowGuests: false,
},
});

Eligibility policy (reusable)

For common eligibility configurations:

{
eligibilityPolicyId: 'policy-club-champs', // Reference to stored policy
}

Eligibility check flow

  1. Check competition-level rules
  2. Check division-specific rules
  3. Validate handicap snapshot available
  4. Verify membership status (if required)
  5. Verify age (if min/max specified)

Team events

Configure team competition

await competitionsService.create({
clubId: 'club-123',
name: 'Better Ball Championship',
format: 'FOUR_BALL_BETTER_BALL',
isTeamEvent: true,
teamSize: 2,
// ...rounds
});

Create team entry

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-a', // Team captain
teamMembers: [
{ playerId: 'player-b', position: 2 },
],
});

4-person team

await entriesService.create({
competitionId: 'comp-123',
playerId: 'player-a',
teamMembers: [
{ playerId: 'player-b', position: 2 },
{ playerId: 'player-c', position: 3 },
{ playerId: 'player-d', position: 4 },
],
});

Team member data

interface CompetitionTeamMember {
id: string;
entryId: string;
playerId: string;
position: number; // 1 = captain, 2-n = members
player: Player;
}

Withdrawals

Withdraw an entry

await entriesService.withdraw({
entryId: 'entry-123',
reason: 'Injury',
});

Withdrawal data

{
withdrawnAt: Date; // Timestamp of withdrawal
withdrawalReason: string; // Optional reason
}

Withdrawal rules

  • Cannot withdraw after round is locked
  • Withdrawal reason is optional but recommended
  • Entry fee refund handled separately (not in tournaments module)
  • Withdrawn entries excluded from leaderboard by default

Include withdrawn in results

const leaderboard = await resultsService.getLeaderboard({
competitionId: 'comp-123',
includeWithdrawn: true, // Include withdrawn entries
});

Entry limits

Max entries

{
maxEntries: 120, // Competition limit
}

Entry creation fails if limit reached.

Entry windows

{
entryOpens: new Date('2025-12-01T00:00:00'),
entryCloses: new Date('2025-12-14T18:00:00'),
}
  • Entries rejected before entryOpens
  • Entries rejected after entryCloses
  • Admin can override windows

Entry fees

Configure fees

{
entryFee: 15000, // R150.00 (cents)
entryFee9Holes: 8000, // R80.00 for 9-hole option
}

Fee tracking

Entry fee payment tracked separately via payments module. The entryFee field is informational for the competition.


Tee sheet sync

Automatically create entries from tee sheet bookings for a linked round:

// Round must have teeSheetId set
await teeSheetSyncService.syncRound('round-123');

Sync behavior

  1. Load round (and teeSheetId); skip if none.
  2. Read tee times + slots for the sheet.
  3. For each slot with playerId, create an entry if one does not already exist for the competition.
  4. Link the entry to the slot (teeTimeSlotId).

Querying entries

List entries for competition

const entries = await entriesService.findByCompetition('comp-123');

List entries for player

const entries = await entriesService.findByPlayer('player-456', {
clubId: 'club-123',
seasonId: 'season-2025',
});

Get entry detail

const entry = await entriesService.findById('entry-123', {
includeScores: true,
includeTeamMembers: true,
});