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.
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 date | Month | Season end year | Reference date |
|---|---|---|---|
| 14 Oct 2026 | October (9) | 2027 | 31 Aug 2027 |
| 1 Apr 2026 | April (4) | 2026 | 31 Aug 2026 |
| 31 Aug 2026 | August (8) | 2026 | 31 Aug 2026 |
| 1 Sep 2026 | September (9) | 2027 | 31 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:
Age group table structure
A typical club configuration might look like:
| code | label | booking_type | age_min | age_max | session_days | capacity |
|---|---|---|---|---|---|---|
u11 | Under 11 | taster | 9 | 10 | ["Tuesday"] | 2 |
u13 | Under 13 | taster | 11 | 12 | ["Tuesday"] | 2 |
u15 | Under 15 | taster | 13 | 14 | ["Tuesday"] | 2 |
u17 | Under 17 | taster | 15 | 16 | ["Tuesday"] | 2 |
u20 | Under 20 | taster | 17 | 19 | ["Tuesday"] | 2 |
academy | Junior Academy | waitlist | 6 | 8 | ["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
activeto include or exclude a group from routing - Update
capacity_per_sessionto change how many athletes can book per session - Update
session_daysto change which days sessions run - Adjust
sort_orderto control tie-breaking when multiple groups could match
booking_type on an existing age group affects all future enquiries immediately. Existing enquiries already routed are not retroactively changed.