Age Group System

Age group management is the core business logic of the EGAC platform. All age-group configuration lives in the age_groups D1 table — nothing is hardcoded in application logic. This means the club can add new groups, change session days, adjust capacity, or enable/disable groups without a code deployment.

UK Athletics age rules

UK Athletics uses a non-calendar age system. An athlete’s age group for any given season is determined by their age on 31 August at the end of that athletics season — not their actual age on the session date, and not their next birthday.

info
Athletics season boundary: A season starting in September runs through to the following August. The reference date is always 31 August of the year the season ends.

Example: A session on 14 October 2026 falls in the 2026/27 season. The reference date is 31 August 2027. An athlete born 12 April 2015 is 12 on that date → U13.

Season year calculation

function athleticsSeasonEndYear(dateIso: string): number {
  const month = new Date(dateIso).getMonth(); // 0-indexed
  const year  = new Date(dateIso).getFullYear();
  // September (month 8) or later → season ends next August
  return month >= 8 ? year + 1 : year;
}
Session dateMonthSeason end yearReference date
14 Oct 2026October (9)202731 Aug 2027
1 Apr 2026April (4)202631 Aug 2026
31 Aug 2026August (8)202631 Aug 2026
1 Sep 2026September (9)202731 Aug 2027

Age group resolution

resolveAgeGroup() takes a DOB and session date, calculates the athlete’s UK Athletics age, and finds the matching active group:

flowchart TD A[DOB + Session Date] --> B[athleticsSeasonEndYear\nmonth >= Sep → year+1, else year] B --> C[Build reference date\nYYYY-08-31] C --> D[ageOnDate\nage in whole years on ref date] D --> E{DOB provided\nand parseable?} E -->|No| F[Return null\n→ route to taster] E -->|Yes| G[Filter active age groups\nsorted by sort_order] G --> H{age >= age_min_aug31\nAND age <= age_max_aug31?} H -->|Match found| I[Return AgeGroupConfig] H -->|No match| F

Age group table structure

A typical club configuration might look like:

codelabelbooking_typeage_minage_maxsession_dayscapacity
u11Under 11taster910["Tuesday"]2
u13Under 13taster1112["Tuesday"]2
u15Under 15taster1314["Tuesday"]2
u17Under 17taster1516["Tuesday"]2
u20Under 20taster1719["Tuesday"]2
academyJunior Academywaitlist68["Saturday","Tuesday"]40

The booking_type column is what drives the routing split — not a hardcoded age threshold.

Session day logic

Session days are stored as a JSON array. getSessionDays() parses this safely:

function getSessionDays(ageGroup: AgeGroupConfig): string[] {
  try {
    return JSON.parse(ageGroup.session_days); // ["Tuesday"] or ["Saturday","Tuesday"]
  } catch {
    return ['Tuesday']; // safe default
  }
}

isValidSessionDay() checks whether a specific date falls on any of the group’s valid days by comparing the day name from date.getDay() against the parsed array.

Upcoming session dates

getNextNSessionDates() returns the next N valid session dates for a group:

// Returns dates as YYYY-MM-DD strings
// Starts from tomorrow — never returns today
getNextNSessionDates(ageGroup, 8); // → ["2026-10-14", "2026-10-21", ...]

For booking invites, the number of weeks is controlled by config.weeks_ahead_booking (default 8). This is the list displayed in the booking invite email and on the /book/:token page.

Capacity per session

Each age group has its own capacity_per_session. The booking API counts existing confirmed/attended bookings for the chosen date and group before accepting a new one:

const capacity = ageGroup.capacity_per_session
  ?? parseInt(config.capacity_per_slot, 10); // fallback to global config
const currentCount = await countBookingsForDateAndGroup(date, ageGroup.id, env);
if (currentCount >= capacity) {
  return err('This session is full', 409, 'SLOT_FULL');
}

Admin management

Age groups are managed in the admin panel at /admin/academy. Admins can:

  • Toggle active to include or exclude a group from routing
  • Update capacity_per_session to change how many athletes can book per session
  • Update session_days to change which days sessions run
  • Adjust sort_order to control tie-breaking when multiple groups could match
warning
Changing booking_type on an existing age group affects all future enquiries immediately. Existing enquiries already routed are not retroactively changed.