Skip to content

Enrolment gating and staff overrides

Active enrolment (and waitlist to active conversion) runs a single evaluation that gathers all blocking issues at once rather than failing on the first check. The API exposes both a preview (dry run, HTTP 200) and enforce paths that return a problem details document when rules still block the action.

The following checks run for create active and waitlist to active:

  1. Age - Activity minimum/maximum age from effective fields; members without a date of birth may fail with enrollments.date_of_birth_required when any bound applies.
  2. Prerequisites - Level prerequisites via completed levels, required skills, or historical enrolments; MemberLevelOverride bypasses prerequisite checks for a level.
  3. Enrolment policies - Required policies for the target activity, scoped via family billing context; if the member is in more than one family, policy evaluation may require an explicit family_id (enrollments.family_context_required).
  4. Capacity - Peak concurrent spot-consuming enrolments over the proposed datetime_range must not exceed effective activity capacity. If any instant in that window would be over capacity (including future-dated enrolments already on the activity), enrollments.capacity_full blocks placement (unless overridden by staff). Blocking issues may include meta.peak_count, meta.capacity, meta.peak_at, and meta.proposed_upper_source.
  5. Session time clash (warning) - For active and casual enrolments with a proposed datetime_range, when the member already has another active or casual enrolment whose timetable sessions overlap in UTC, enrollments.schedule_conflict is returned with severity: warning. This does not block checkout or conversion; staff and family portal clients may proceed without an override. meta includes conflicting_activity_id and conflicting_activity_name. This is separate from enrollments.overlapping_enrollment, which blocks duplicate enrolment periods on the same activity.

Each issue is returned with severity (for example blocking or warning), human-readable title / detail, optional pointer and meta, plus can_override and required_permission after annotation.

Staff with the correct Django permission can attest that a specific gate may be bypassed for a given request by sending staff_overrides: a map from issue code to a truthy value (for example { "enrollments.capacity_full": true }).

Rules:

  • Overrides apply per issue code. If staff_overrides[code] is not set, the issue remains.
  • The server checks required_permission for that code. If the user lacks it, the issue is replaced with enrollments.insufficient_permission_to_override (meta.original_code records the gate).
  • Warnings are not overridable via this map (can_override stays false).

Permissions used for enrolment gates (codenames on ActivityEnrollment):

Gate area Permission codename
Age / DOB activities.override_enrollment_age
Prerequisites activities.override_enrollment_prerequisites
Policies activities.override_enrollment_policies
Capacity activities.override_enrollment_capacity

Capacity uses a Postgres tstzrange peak query over the proposed enrolment window (datetime_range on the preview or create payload). Structural toggles control whether active and casual enrolments count (configurable per organisation, location, and category). Enrolments that carry labels (for example Trial or Makeup) can also be capped by per-label capacity rules resolved for the activity.

When the proposed upper bound is omitted (ongoing enrolment), the server normalises the window using sibling enrolments on the activity (latest finite end or latest start), or an open-ended range when none exist.

Waitlist enrolments do not consume capacity today. Committed destination enrolments from transfers are included via their stored datetime_range rows.

POST .../eligibility-preview evaluates gates and returns:

  • issues: the same annotated issue objects you would see inside a problem errors array (field names align for client reuse).
  • can_proceed: true when there are no blocking issues.

The preview does not mutate enrolments. It does not apply staff_overrides; it only reflects whether the current actor is staff for can_override hints.

New active, casual, trial, and waitlist enrolments with initial charges must be created through staff Checkout, not POST .../enrollments/. That nested create endpoint returns 405 Method Not Allowed.

Use POST .../eligibility-preview (and live Cart preview via GET /api/v1/carts/{id}/) to evaluate gates before starting Checkout. Staff may attest overrides per issue code when freezing the Cart (POST /api/v1/carts/{id}/checkout/) by sending staff_overrides: a map from cart line id (or client_key) to { "<issue_code>": true }. The server re-validates gates at freeze and applies the same override rules as direct enrolment create.

Converting waitlist to active applies staff_overrides for staff users. If blocking issues remain, the API responds with 422 Unprocessable Entity, Content-Type: application/problem+json, and problem type:

https://docs.keja.co/reference/problem/enrollments.gates_failed

See the dedicated page for that type and full JSON shape: Enrolment gates failed (enrollments.gates_failed).

  • Drive UI and retry logic from each issue’s code, can_override, and required_permission, not from parsing detail strings.
  • For policy gates when a member belongs to multiple families, pass family_id on create/preview payloads when the API expects family-scoped policy context.
  • Re-run preview after changes (DOB, policy acceptance, family selection) before submitting an enrolment.