Academy Waitlist
The Junior Academy is a structured programme with limited seasonal places. Athletes whose age group has booking_type = 'waitlist' are placed on the Academy waitlist rather than sent a taster booking invite. Admission is managed by admins who invite athletes from the waitlist; parents respond via a unique link in their email.
Season model
Each Academy season has a start date, end date, and capacity. Seasons progress through four statuses:
Typically one season runs per year, approximately April to August. Multiple seasons can exist simultaneously for different age groups (each academy_season links to one age_group_id).
Waitlist entry lifecycle
Joining the waitlist
When an enquiry is routed to the Academy path (booking_type = 'waitlist'):
- A row is inserted in
academy_waitlistlinked to the current open season (if one exists) positionis set to the next sequential number within the season (FCFS ordering)- The
academy_waitlistemail template is sent with a response URL:/academy/respond/:token - The enquiry event
academy_waitlist_addedis appended
E->>DB: getOpenSeason(age_group_id) E->>DB: INSERT academy_waitlist\n(season_id, position=next, status=waiting) E->>Email: sendAcademyWaitlist(enquiry, entry) Email—>>E: { success: true } E->>DB: UPDATE waitlist SET sent_at=now E->>DB: Append event: academy_waitlist_added
Parent response
When an admin is ready to offer places, they mark selected waitlist entries as invited. Parents receive the response email containing a unique link to /academy/respond/:token.
The POST /api/academy/respond endpoint:
- Validates the token exists and the entry hasn’t already been responded to
- Records the response (
yesorno) and setsresponded_at - Updates status to
acceptedordeclined - Appends
academy_response_receivedevent to the enquiry - Returns a confirmation message
The endpoint is idempotent — if a parent clicks the link again after responding, it returns their previous response without error.
// Idempotency check
if (invitation.status === 'accepted' || invitation.status === 'declined') {
return ok({
message: `Your response (${invitation.response}) has already been recorded.`,
already_responded: true,
});
}
Season rollover cron
POST /api/cron/academy-rollover runs on the 1st of each month at 09:00 UTC.
It finds all seasons with status = 'open' that have passed their end_date, then for each:
- Collects all
waitingandinvited(unplaced) entries - For each unplaced entry, checks whether the athlete was
acceptedin any prior season (is_returning) - Creates a new waitlist entry in the next upcoming season (or with
season_id = nullif no upcoming season exists yet) - Sets the old season status to
closed
D —> E[For each expired season] E —> F[List unplaced entries\nstatus = waiting or invited] F —> G[For each unplaced entry] G —> H{hasAcceptedEntry\nfor this enquiry?} H —>|Yes| I[is_returning = 1] H —>|No| J[is_returning = 0] I —> K[rolloverWaitlistEntry\ninto next season] J —> K K —> L{More entries?} L —>|Yes| G L —>|No| M[updateSeasonStatus → closed] M —> N{More seasons?} N —>|Yes| E N —>|No| END2[Return totals]
The rollover is idempotent: it only processes seasons where status = 'open' and end_date < today. Running it multiple times on the same day has no effect after the first run because the season status is set to closed.
Returning athlete priority
The is_returning flag on a waitlist entry indicates the athlete attended the club in a prior season. This field is available to admin filtering and reporting, allowing the club to prioritise returning athletes when allocating limited places.
Admin management
The admin panel at /admin/academy shows:
- Current season status and capacity
- Waitlist with position, athlete name, DOB, status, and
is_returningflag - Controls to invite, mark ineligible, or withdraw entries
The GET /api/admin/academy endpoint supports filtering by season_id, status, and search (name/email).