Academic Year Rollover API Reference
Complete API contracts for Academic Year Rollover, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Academic Year Rollover - API Reference
Audience: Admin panel engineers, QA, and backend engineers integrating with rollover. Scope: Admin-only APIs owned by the Rollover module — no public, mobile, or webhook surface exists.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | cycles/admin, items/admin, allocation/admin, processing/admin, activation/admin, corrections/admin, settings/admin, insights/admin controllers | Routes, guards, permissions |
| DTOs | */dto/*.ts under each submodule | Request/response/query shapes |
| Services | */core/*.service.ts, shared/*.service.ts | Behavior, side effects, error conditions |
| Schema | packages/db/src/schema/school/rollover.ts, academic-sessions.ts | Enum values, persisted fields |
| Error registry | apps/api/src/common/types/error-codes.ts, shared/rollover-codes.ts | All error and outcome codes |
2. Module Summary
| Field | Value |
|---|---|
| Module name | Rollover |
| Module slug | rollover |
| Primary actors | admin, superadmin, internal worker |
| API surfaces | admin only |
| Base route prefixes | /api/rollover/..., /api/rollover/settings |
| Auth model | JwtAuthGuard (global) + RoleGuard against @Permissions() codes |
| Persistence | PostgreSQL only — no cache, no MongoDB, no realtime |
| Runtime source of truth | rollover_cycles, rollover_items, student_graduations, rollover_events, rollover_settings |
| Sibling docs | Backend, Features and flows, Promotions |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
| Cycle | One rollover run from a source session to a target session | rollover-cycle.dto.ts | Every cycle/item/allocation/processing endpoint |
| Item | One pupil's row within a cycle — their source snapshot, target assignment, and status | rollover-item.dto.ts | Items, allocation, corrections |
| Inclusion | Whether an item participates in this run at all (included/excluded) | rollover.ts | Items |
| Display status | A computed, UI-facing status (processing, completed_with_exceptions, etc.) derived from status/runState/runOutcome | rollover-cycle.dto.ts | Cycle detail, progress |
| Fingerprint | A hash of everything that would change the outcome of processing, used to detect a stale plan | rollover-fingerprint.service.ts | Review, process |
| Proposal token | A token returned by an allocation preview and required, unchanged, on the matching apply call | rollover-allocation.dto.ts | Section/roll allocation |
| Lease | The exclusivity token a worker holds while processing a cycle | rollover_cycles.lease_token | Processing, progress |
| Preflight | The set of blocking/warning codes checked before activation is allowed | rollover-activation.dto.ts | Activation |
4. API Surface Map
| Surface | Method | Path | Actor | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|
| Admin | GET | /rollover/overview | Admin | Rollover_READ | RolloverCyclesController | Dashboard: current/target session, active cycle, counts |
| Admin | GET | /rollover/cycles | Admin | Rollover_READ | RolloverCyclesController | List cycles |
| Admin | POST | /rollover/cycles | Admin | Rollover_CREATE | RolloverCyclesController | Start a cycle |
| Admin | GET | /rollover/cycles/:publicId | Admin | Rollover_READ | RolloverCyclesController | Cycle detail with readiness |
| Admin | DELETE | /rollover/cycles/:publicId | Admin | Rollover_DELETE | RolloverCyclesController | Cancel a cycle |
| Admin | POST | /rollover/cycles/:publicId/sync | Admin | Rollover_UPDATE | RolloverCyclesController | Re-sync roster |
| Admin | POST | /rollover/cycles/:publicId/review | Admin | Rollover_UPDATE | RolloverCyclesController | Prepare fingerprint for processing |
| Admin | GET | /rollover/cycles/:publicId/items | Admin | Rollover_READ | RolloverItemsController | List items with filters |
| Admin | GET | /rollover/cycles/:publicId/exceptions | Admin | Rollover_READ | RolloverItemsController | Grouped exceptions/warnings with samples |
| Admin | PATCH | /rollover/cycles/:publicId/items/:studentPublicId | Admin | Rollover_UPDATE | RolloverItemsController | Edit one item's target class/roll |
| Admin | POST | /rollover/cycles/:publicId/items/assign-class | Admin | Rollover_UPDATE | RolloverItemsController | Bulk assign target class |
| Admin | POST | /rollover/cycles/:publicId/items/exclude | Admin | Rollover_UPDATE | RolloverItemsController | Manually exclude pupils |
| Admin | POST | /rollover/cycles/:publicId/items/include | Admin | Rollover_UPDATE | RolloverItemsController | Re-include pupils |
| Admin | POST | /rollover/cycles/:publicId/items/capacity-override | Superadmin | RolloverOverrides_UPDATE | RolloverItemsController | Place beyond section capacity |
| Admin | GET | /rollover/cycles/:publicId/sections | Admin | Rollover_READ | RolloverAllocationController | Target classes with demand/capacity |
| Admin | POST | /rollover/cycles/:publicId/section-allocation/preview | Admin | Rollover_UPDATE | RolloverAllocationController | Preview section allocation |
| Admin | POST | /rollover/cycles/:publicId/section-allocation/apply | Admin | Rollover_UPDATE | RolloverAllocationController | Apply a previewed section allocation |
| Admin | POST | /rollover/cycles/:publicId/roll-allocation/preview | Admin | Rollover_UPDATE | RolloverAllocationController | Preview roll allocation |
| Admin | POST | /rollover/cycles/:publicId/roll-allocation/apply | Admin | Rollover_UPDATE | RolloverAllocationController | Apply a previewed roll allocation |
| Admin | POST | /rollover/cycles/:publicId/process | Superadmin | RolloverProcessing_CREATE | RolloverProcessingController | Start/re-run background processing |
| Admin | GET | /rollover/cycles/:publicId/progress | Admin | Rollover_READ | RolloverProcessingController | Poll run progress |
| Admin | GET | /rollover/cycles/:publicId/activation-preflight | Admin | Rollover_READ | RolloverActivationController | Preflight blocking/warning codes |
| Admin | POST | /rollover/cycles/:publicId/activate | Superadmin | RolloverActivation_UPDATE | RolloverActivationController | Make the target session current |
| Admin | POST | /rollover/cycles/:publicId/finalize | Superadmin | RolloverActivation_UPDATE | RolloverActivationController | Finalize the cycle |
| Admin | POST | /rollover/cycles/:publicId/notify-placements | Superadmin | RolloverActivation_UPDATE | RolloverActivationController | Retry placement notifications stuck in failed or unsent none |
| Admin | POST | /rollover/sessions/:sessionPublicId/lock | Superadmin | AcademicYearLock_UPDATE | RolloverActivationController | Lock a historical session |
| Admin | POST | /rollover/sessions/:sessionPublicId/unlock | Superadmin | AcademicYearLock_UPDATE | RolloverActivationController | Unlock a session |
| Admin | POST | /rollover/cycles/:publicId/items/:studentPublicId/correct | Superadmin | RolloverCorrections_UPDATE | RolloverCorrectionsController | Move, realign, or relink a pupil's placement |
| Admin | GET | /rollover/settings | Superadmin | RolloverSettings_READ | RolloverSettingsController | Read rollover settings |
| Admin | PATCH | /rollover/settings | Superadmin | RolloverSettings_UPDATE | RolloverSettingsController | Update rollover settings |
| Admin | GET | /rollover/cycles/:publicId/events | Admin | Rollover_READ | RolloverInsightsController | Event timeline, paginated |
| Admin | GET | /rollover/cycles/:publicId/integrity | Admin | Rollover_READ | RolloverInsightsController | Integrity cross-check report |
| Admin | GET | /rollover/cycles/:publicId/id-cards | Admin | Rollover_READ | RolloverInsightsController | ID-card readiness counts |
| Admin | GET | /rollover/cycles/:publicId/new-admissions | Admin | Rollover_READ | RolloverInsightsController | New admissions not covered by the cycle |
All three correction actions (move, realign, relink) share the single POST .../items/:studentPublicId/correct route; the action field of the request body selects the case. See Backend §6.9 for the write sequence each action follows.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Everyday cycle/item/allocation work | JwtAuthGuard, RoleGuard | req.user.id, activeRole | Rollover_* | No | Not superadmin-only by default — a school can grant it to any admin role. |
| Processing | JwtAuthGuard, RoleGuard | req.user.id | RolloverProcessing_CREATE | No | Superadmin-only by default. |
| Capacity override | JwtAuthGuard, RoleGuard | req.user.id | RolloverOverrides_UPDATE | No | Superadmin-only by default. |
| Corrections | JwtAuthGuard, RoleGuard | req.user.id | RolloverCorrections_UPDATE | No | Superadmin-only by default. |
| Activation / finalize | JwtAuthGuard, RoleGuard | req.user.id | RolloverActivation_UPDATE | No | Superadmin-only by default. |
| Lock / unlock a session | JwtAuthGuard, RoleGuard | req.user.id | AcademicYearLock_UPDATE | No | Superadmin-only by default. |
| Settings | JwtAuthGuard, RoleGuard | req.user.id | RolloverSettings_READ/_UPDATE | No | Superadmin-only by default. |
No endpoint in this module supports guest or unauthenticated access; there is no public or mobile surface.
6. DTO and Model Reference
6.1 CreateRolloverCycleDto — body, POST /rollover/cycles
| Field | Type | Required | Notes |
|---|---|---|---|
sourceSessionPublicId | uuid | Yes | Must differ from targetSessionPublicId. |
targetSessionPublicId | uuid | Yes | Must start after the source session. |
promotionCyclePublicId | uuid | Yes | Must be finalized. |
6.2 ListRolloverCyclesQueryDto — query, GET /rollover/cycles
| Field | Type | Required | Notes |
|---|---|---|---|
status | enum (preparing,processed,activated,finalized,cancelled) | No | Filters by raw status. |
6.3 RolloverCycleDetailDto — response, GET /rollover/cycles/:publicId
| Field | Type | Notes |
|---|---|---|
publicId, code, status, displayStatus | string | displayStatus is computed — see Backend §5.2. |
sourceSession, targetSession | RolloverSessionRefDto | {publicId, name} |
readiness | RolloverReadinessDto[] | 15 checks across 7 groups — see Backend §6.8. |
counts | object | Item counts by status. |
6.4 RolloverOverviewDto — response, GET /rollover/overview
| Field | Type | Notes |
|---|---|---|
currentSession | RolloverSessionRefDto | null | — |
suggestedTarget | RolloverSessionRefDto | null | — |
promotionCycle | RolloverPromotionCycleRefDto | null | — |
activeCycle | RolloverCycleSummaryDto | null | Readiness omitted from this summary shape. |
students | RolloverOverviewStudentCountsDto | activeInSource, withdrawnInSource, transferredInSource, inactiveRecords, newAdmissionsInTarget, missingSourcePlacement. |
decisions | RolloverOverviewDecisionCountsDto | total, byOutcome[], withoutDecision. |
recentCycles | RolloverCycleSummaryDto[] | — |
6.5 ListRolloverItemsQueryDto — query, GET /rollover/cycles/:publicId/items
| Field | Type | Required | Notes |
|---|---|---|---|
view | enum (ready,exceptions,excluded,processed,all) | No | Preset filter combinations. |
outcome | string | No | Filters by promotion outcome. |
status | string | No | Filters by item status. |
exceptionCode | string | No | Filters items carrying a specific exception code. |
sourceClassPublicId | uuid | No | — |
targetGradePublicId | uuid | No | — |
targetClassPublicId | uuid | No | — |
6.6 UpdateRolloverItemDto — body, PATCH /rollover/cycles/:publicId/items/:studentPublicId
| Field | Type | Required | Notes |
|---|---|---|---|
targetClassPublicId | uuid | No | — |
plannedRollNumber | integer 1–999 | No | Only settable when roll policy is not fully automatic; see ROLLOVER_ROLL_POLICY_AUTOMATIC. |
6.7 ExcludeRolloverItemsDto / IncludeRolloverItemsDto
| Field | Type | Required | Notes |
|---|---|---|---|
studentPublicIds | uuid[] | Yes | Batch of pupils to exclude/include. |
reason | string | Yes (exclude only) | Persisted as exclusionReason. |
6.8 CapacityOverrideDto — body, POST /rollover/cycles/:publicId/items/capacity-override
| Field | Type | Required | Notes |
|---|---|---|---|
studentPublicId | uuid | Yes | — |
enable | boolean | Yes | Sets/clears capacityOverride. |
reason | string | Yes when enable=true | — |
6.9 SectionAllocationPreviewDto / SectionAllocationResultDto
| Field | Type | Required | Notes |
|---|---|---|---|
targetGradePublicId | uuid | Yes | — |
strategy | enum (balanced,keep_previous,alphabetical,previous_roll) | Yes | See Backend §6.4 for algorithm detail. |
onlyUnassigned | boolean | No | Restricts the proposal to items with no targetClassId yet. |
preserveShift | boolean | No | Restricts eligibility to same-shift classes where possible. |
Result: proposalToken (string, required unchanged on apply), gradePublicId, strategy, perClass[] (per-class assignment counts), assignments[] (per-student proposed class), unassignedCount (int), applied (boolean, present once applied). Each perClass[] entry carries genderAfter: { male, female, other } only when rollover_settings.balanceGender was on for this allocation AND the caller holds Students_READ — it is omitted entirely otherwise, never returned as null.
6.10 RollAllocationPreviewDto / RollAllocationResultDto
| Field | Type | Required | Notes |
|---|---|---|---|
targetGradePublicId | uuid | Yes | — |
strategy | enum (alphabetical,previous_roll,keep_previous) | Yes | See Backend §6.5. |
scope | enum (section,grade) | Yes | Whether roll numbers are unique per class or per grade. |
Result: proposalToken, gradePublicId, strategy, scope, assignments[], warnings[], applied.
6.11 AllocationApplyDto — body, both apply endpoints
| Field | Type | Required | Notes |
|---|---|---|---|
proposalToken | string | Yes | Must match the token from the matching preview call, or ROLLOVER_PROPOSAL_STALE. |
6.12 RolloverProcessRequestDto — body, POST /rollover/cycles/:publicId/process
| Field | Type | Required | Notes |
|---|---|---|---|
fingerprint | string, min length 1 | Yes | The fingerprint returned by the review call; must equal the fingerprint recomputed live under lock. |
confirm | boolean | Yes, must be true | ROLLOVER_CONFIRMATION_REQUIRED if false/absent. |
6.13 RolloverProcessResultDto — response, same endpoint
| Field | Type | Notes |
|---|---|---|
cyclePublicId | uuid | — |
attempt | integer | The attempt number just started. |
itemsToRun | integer | Count of non-terminal included items about to be processed. |
6.14 RolloverProgressDto — response, GET /rollover/cycles/:publicId/progress
| Field | Type | Notes |
|---|---|---|
displayStatus | string | Computed display status. |
runState | enum (idle,running) | — |
runOutcome | enum (completed,completed_with_exceptions,partially_completed) | null | Set once finished. |
failureCode | string | null | One of the run failure codes in Backend §13. |
attempt | integer | — |
runStartedAt, runFinishedAt, lastHeartbeatAt | timestamp | null | — |
totalIncluded | integer | — |
counts | RolloverProgressCountsDto | Per-status item counts. |
excluded | integer | — |
6.15 RolloverSettingsDto / UpdateRolloverSettingsDto
| Field | Type | Default | Notes |
|---|---|---|---|
allowConditionalPlacement | boolean | true | — |
allowPlacementWithoutSection | boolean | true | — |
capacityPolicy | enum (block,allow_override) | block | — |
defaultSectionStrategy | enum (balanced,keep_previous,alphabetical,previous_roll) | balanced | — |
preserveShift | boolean | true | — |
rollStrategy | enum (alphabetical,previous_roll,keep_previous) | alphabetical | — |
rollScope | enum (section,grade) | section | — |
allowActivationWithExceptions | boolean | true | — |
notifyPlacement | boolean | true | — |
placementChannels | array of email|sms|push|in_app | ['in_app','push'] | — |
adminChannels | array of email|sms|push|in_app | ['in_app'] | — |
idCardReminder | boolean | true | — |
balanceGender | boolean | false | Whether the balanced section strategy also evens out male/female counts per section. |
graduateAccessDays | number | 180 | Days a graduated pupil keeps read-only portal access after graduated_on, 0–3650. Copied onto the graduation at the moment it is recorded, so changing this never shortens or extends access already granted. |
version | bigint | — | Required on PATCH for optimistic locking; mismatch is ROLLOVER_SETTINGS_STALE. |
updatedAt | timestamp | — | Response-only. |
UpdateRolloverSettingsDto makes every field above optional except version, which is always required on a write.
6.16 RolloverCorrectionDto / RolloverCorrectionResultDto — POST /rollover/cycles/:publicId/items/:studentPublicId/correct
| Field | Type | Required | Notes |
|---|---|---|---|
action | enum (move,realign,relink) | Yes | Selects which correction case runs. |
targetClassPublicId | UUID | Only for move, and for realign cases that place into a new class | Ignored by relink. |
reason | string, 3–1000 chars | Yes | Recorded on the rollover_events row. |
allowOverCapacity | boolean | No | move only. Enrolls into the target class over its capacity when rollover_settings.capacity_policy is allow_override and the caller holds RolloverOverrides_UPDATE. Checked in order: the permission first (403 ROLLOVER_OVER_CAPACITY_NOT_PERMITTED if missing, before the transaction opens, regardless of action or the current policy), then that action === "move" (400 ROLLOVER_CORRECTION_INVALID otherwise), then the capacity policy itself (409 ROLLOVER_OVERRIDE_NOT_ALLOWED if not allow_override). |
RolloverCorrectionResultDto response: { item: RolloverItemDto | null } — the item's post-correction state, re-read after the transaction commits. A move performed with allowOverCapacity: true records overCapacity: true|false in the placement_corrected event's toValue, reflecting whether the destination class actually ended up over capacity after the enrollment; the field is absent from an ordinary move's event. Un-graduating a pupil through realign (cases d/e) always restores their pupil-scope role grants, and restores sign-in as well only when the actor performing the realign holds Users_UPDATE.
7. Enum Reference
| Enum | Values | Source |
|---|---|---|
| Cycle status | preparing, processed, activated, finalized, cancelled | rollover.ts |
| Cycle display status | cancelled, preparing, processing, completed, completed_with_exceptions, partially_completed, activated, finalized | rollover-cycles.service.ts |
| Run state | idle, running | rollover.ts |
| Run outcome | completed, completed_with_exceptions, partially_completed | rollover.ts |
| Placement notification state | none, queued, sent, failed | rollover.ts |
| Item status | pending, placed, graduated, awaiting_section, needs_review, failed, withdrawn | rollover.ts |
| Inclusion | included, excluded | rollover.ts |
| Exclusion source | manual, decision | rollover.ts |
| Capacity policy | block, allow_override | rollover.ts |
| Section strategy | balanced, keep_previous, alphabetical, previous_roll | rollover.ts |
| Roll strategy | alphabetical, previous_roll, keep_previous | rollover.ts |
| Roll scope | section, grade | rollover.ts |
| Item view filter | ready, exceptions, excluded, processed, all | rollover-item.dto.ts |
| Notification channel | email, sms, push, in_app | rollover.ts (CHECK constraint) |
Exception, warning, outcome, run-failure, integrity, preflight, and allocation-unassigned codes are enumerated in full in Backend §13 — they are not HTTP-facing enums but do appear verbatim in response payloads (exceptionCodes[], outcomeCode, failureCode, integrity report entries, preflight code lists).
8. Endpoint Reference
8.1 GET /rollover/overview
Purpose: The rollover dashboard's landing data — which session is current, what the suggested next target is, whether a cycle is already active, and headline counts of pupils and decisions. Call this first when opening the rollover screen.
Auth: Rollover_READ.
Response: RolloverOverviewDto — see §6.4.
Errors: none domain-specific — a caller lacking permission gets 403.
8.2 GET /rollover/cycles
Purpose: List past and present rollover cycles, optionally filtered by status, for the cycle-history screen.
Auth: Rollover_READ.
Query: status (optional).
Response: paginated array of RolloverCycleSummaryDto.
8.3 POST /rollover/cycles
Purpose: Start a new rollover cycle for a source/target session pair once the source's promotion cycle is finalized. This is the entry point to the whole module.
Auth: Rollover_CREATE.
Body: CreateRolloverCycleDto — see §6.1.
{
"sourceSessionPublicId": "b1a2...",
"targetSessionPublicId": "c3d4...",
"promotionCyclePublicId": "e5f6..."
}Side Effects: inserts a rollover_cycles row and syncs the roster into rollover_items in the same operation.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
400 | Validation | Missing/invalid public IDs |
409 | ROLLOVER_SAME_SESSION | Source equals target |
409 | ROLLOVER_TARGET_BEFORE_SOURCE | Target does not start after source |
409 | ROLLOVER_TARGET_LOCKED | Target session is locked |
409 | ROLLOVER_TARGET_INACTIVE | Target session is not active |
409 | ROLLOVER_CYCLE_EXISTS | A live cycle already targets this pair |
409 | ROLLOVER_PROMOTION_NOT_FINALIZED | Source promotion cycle isn't finalized |
8.4 GET /rollover/cycles/:publicId
Purpose: The full cycle detail view, including the 15-check readiness checklist, for the cycle's own screen.
Auth: Rollover_READ. Response: RolloverCycleDetailDto — see §6.3.
Error Cases: 404 ROLLOVER_CYCLE_NOT_FOUND.
8.5 DELETE /rollover/cycles/:publicId
Purpose: Cancel a cycle before any placement or graduation has been made — for abandoning a rollover started against the wrong session pair or promotion cycle.
Auth: Rollover_DELETE.
Side Effects: status = cancelled, cancelled_at/cancelled_by_user_id/cancel_reason set, cycle_cancelled event inserted.
Error Cases: 409 ROLLOVER_CANCEL_NOT_ALLOWED if any item is already placed/graduated.
8.6 POST /rollover/cycles/:publicId/sync
Purpose: Re-pull the roster from the promotion cycle and the source session's live enrollments — call this whenever a promotion decision changes or a pupil's source enrollment changes after the cycle was created.
Auth: Rollover_UPDATE. Response: {inserted: number, updated: number}.
Side Effects: see Backend §6.3 for the exact insert/update rules, including auto-exclusion and manual-exclusion preservation.
8.7 POST /rollover/cycles/:publicId/review
Purpose: Snapshot the current settings and compute the confirmation fingerprint that POST /rollover/cycles/:publicId/process will later require, unchanged. Call this immediately before processing, after the admin is satisfied with the roster and allocations.
Auth: Rollover_UPDATE. Response (RolloverReviewResultDto): fingerprint, countsByStatus (array), countsByOutcome (array), excludedCount, exceptionCount, warningCount, toProcess, sections — a plain RolloverSectionGradeDemandDto[] of per-grade class capacity/demand (not wrapped in a {grades} object), rollConflicts, readiness (array), and summary: {sourceSessionName, targetSessionName}.
8.8 GET /rollover/cycles/:publicId/items
Purpose: List items for the cycle's roster screen, with the view/outcome/status/exceptionCode/class/grade filters.
Auth: Rollover_READ. Query: ListRolloverItemsQueryDto — see §6.5.
Edge Cases: an unrecognized view value is rejected by DTO validation (400); an empty result set for a valid filter returns an empty paginated array, not an error.
8.9 GET /rollover/cycles/:publicId/exceptions
Purpose: A grouped summary of every exception and warning code currently present in the cycle, with sample pupils per group — the screen an admin uses to triage before processing.
Auth: Rollover_READ. Response: {groups: [{code, severity: "error"|"warning", count, samples: RolloverItemSummaryDto[]}], total}. count is an exact per-code count computed in SQL over the scoped item set (included, non-terminal); samples is capped at 50 rows per code and never drives count or total. total is the count of scoped items carrying at least one exception or warning code.
8.10 PATCH /rollover/cycles/:publicId/items/:studentPublicId
Purpose: Edit a single pupil's target class and/or planned roll number by hand, for the exception-resolution workflow.
Auth: Rollover_UPDATE. Body: UpdateRolloverItemDto — see §6.6.
Error Cases: 404 ROLLOVER_ITEM_NOT_FOUND; 400 ROLLOVER_CLASS_INVALID if the class doesn't belong to the target grade/session; 400 ROLLOVER_ROLL_INVALID if the roll is out of range or already taken in scope; 409 ROLLOVER_ROLL_POLICY_AUTOMATIC if manual roll entry conflicts with settings.
8.11 POST /rollover/cycles/:publicId/items/assign-class
Purpose: Bulk-assign the same target class to a batch of pupils, for when an admin wants to hand-place a group without running the full allocation algorithm.
Auth: Rollover_UPDATE. Body: {studentPublicIds: uuid[], targetClassPublicId: uuid}.
8.12 POST /rollover/cycles/:publicId/items/exclude
Purpose: Manually exclude a batch of pupils from this rollover run — for pupils who are leaving the school, on long leave, or otherwise should not be placed even though their decision says otherwise.
Auth: Rollover_UPDATE. Body: ExcludeRolloverItemsDto — see §6.7. Requires reason.
Side Effects: inclusion = excluded, exclusionSource = manual; a later POST /rollover/cycles/:publicId/sync never reverts this.
8.13 POST /rollover/cycles/:publicId/items/include
Purpose: Reverse a manual exclusion.
Auth: Rollover_UPDATE. Body: IncludeRolloverItemsDto.
8.14 POST /rollover/cycles/:publicId/items/capacity-override
Purpose: Allow a specific pupil to be placed into a section that is already at capacity — for the rare case where a physical class genuinely needs to exceed its configured capacity.
Auth: RolloverOverrides_UPDATE (superadmin-only by default). Body: CapacityOverrideDto — see §6.8. reason required when enabling.
Error Cases: 403 ROLLOVER_OVERRIDE_NOT_ALLOWED when capacityPolicy = block and the caller lacks the permission.
8.15 GET /rollover/cycles/:publicId/sections
Purpose: Per-grade target class capacity/demand snapshot — the input an admin reviews before running a section-allocation preview.
Auth: Rollover_READ. Response: array of {classPublicId, capacity, occupied, planned, remaining} — see Backend §6.13.
8.16 POST /rollover/cycles/:publicId/section-allocation/preview
Purpose: Compute (without writing) how a chosen strategy would distribute one grade's pupils across its target classes.
Auth: Rollover_UPDATE. Body: SectionAllocationPreviewDto — see §6.9.
{
"targetGradePublicId": "a1b2...",
"strategy": "balanced",
"onlyUnassigned": true,
"preserveShift": true
}Response: SectionAllocationResultDto with a proposalToken that must be replayed to apply.
Edge Cases: a grade with zero eligible classes returns every pupil unassigned with reason NO_CLASS; see the worked examples in Backend §6.4.
8.17 POST /rollover/cycles/:publicId/section-allocation/apply
Purpose: Commit a previously previewed section-allocation proposal.
Auth: Rollover_UPDATE. Body: AllocationApplyDto (proposalToken).
Error Cases: 409 ROLLOVER_PROPOSAL_STALE if the cycle's state changed since the matching preview.
8.18 POST /rollover/cycles/:publicId/roll-allocation/preview
Purpose: Compute (without writing) roll number assignments for one grade under a chosen strategy and scope.
Auth: Rollover_UPDATE. Body: RollAllocationPreviewDto — see §6.10.
8.19 POST /rollover/cycles/:publicId/roll-allocation/apply
Purpose: Commit a previously previewed roll-allocation proposal.
Auth: Rollover_UPDATE. Body: AllocationApplyDto.
Error Cases: 409 ROLLOVER_PROPOSAL_STALE.
8.20 POST /rollover/cycles/:publicId/process
Purpose: Start (or resume) background placement processing for every included, non-terminal item in the cycle. This is the point of no easy return — it is superadmin-only and requires an explicit, freshly-computed fingerprint.
Auth: RolloverProcessing_CREATE (superadmin-only by default). Body: RolloverProcessRequestDto — see §6.12.
{
"fingerprint": "b6e1c9...",
"confirm": true
}Response: 201, RolloverProcessResultDto — see §6.13.
Side Effects: writes an outbox row that enqueues AcademicYearJob.PROCESS_ROLLOVER; no direct write to enrollments happens synchronously in this request.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
400 | Validation | confirm missing or false |
409 | ROLLOVER_PREVIEW_STALE | Fingerprint mismatch — state changed since review |
409 | ROLLOVER_NOTHING_TO_PROCESS | No non-terminal included items |
409 | ROLLOVER_INVALID_STATE | A run is already in progress for this cycle |
8.21 GET /rollover/cycles/:publicId/progress
Purpose: Poll the state of an in-progress or just-finished processing run, for a progress bar on the admin screen.
Auth: Rollover_READ. Response: RolloverProgressDto — see §6.14.
8.22 GET /rollover/cycles/:publicId/activation-preflight
Purpose: Check whether the target year can be activated right now, and why not if it can't.
Auth: Rollover_READ. Response: {blocking: string[], warnings: string[]} drawn from the preflight code lists in Backend §13.
8.23 POST /rollover/cycles/:publicId/activate
Purpose: Make the target session current and queue placement notifications. The single most consequential action in the module — it moves the entire school forward a year.
Auth: RolloverActivation_UPDATE (superadmin-only by default).
Side Effects: academic_sessions.is_current flips (source → target) via switchCurrent(..., { via: "rollover" }); cycle status = activated; NOTIFY_PLACEMENTS enqueued if notifyPlacement is on and unnotified placements exist.
Error Cases: 409 ROLLOVER_ACTIVATION_BLOCKED if any preflight blocking code is present; 409 ROLLOVER_WARNINGS_UNACKNOWLEDGED if allowActivationWithExceptions = false and warnings remain.
8.24 POST /rollover/cycles/:publicId/finalize
Purpose: Close out an activated cycle, optionally locking the source session at the same time.
Auth: RolloverActivation_UPDATE (superadmin-only by default). Body: {lockSource?: boolean, lockReason?: string}.
Side Effects: status = finalized; if lockSource is true, the source session's locked_at/locked_by_user_id/lock_reason are set.
8.25 POST /rollover/sessions/:sessionPublicId/lock
Purpose: Lock a session independent of finalize — for locking a historical year at any later point.
Auth: AcademicYearLock_UPDATE (superadmin-only by default). Body: {reason: string} (3–1000 chars).
Error Cases: 409 if the session is currently is_current (a current session can never be locked, by database constraint).
8.26 POST /rollover/sessions/:sessionPublicId/unlock
Purpose: Reverse a lock.
Auth: AcademicYearLock_UPDATE (superadmin-only by default).
8.27 POST /rollover/cycles/:publicId/notify-placements
Purpose: Retry placement notifications for a cycle whose placementNotificationState is stuck at failed, or unsent at none on an already-activated/finalized cycle whose settings have notifyPlacement on.
Auth: RolloverActivation_UPDATE (superadmin-only by default).
Side Effects: sets rollover_cycles.placement_notification_state = 'queued', increments notification_attempt, and enqueues an outbox event (aggregateType: "rollover_cycle", eventType: "rollover.notify_placements_requested") on QueueName.ACADEMIC_YEAR with jobName: AcademicYearJob.NOTIFY_PLACEMENTS and dedupe key rollover-notify-${cyclePublicId}-${attempt} — the attempt number is part of the key, so each retry gets its own dedupe slot rather than colliding with the one it is retrying. A placement_notifications_queued row is written to rollover_events. The worker (shared/placement-notification.service.ts) then sends the rollover.placement_changed notification to guardians of every placed, un-notified item.
Error Cases: 409 ROLLOVER_NOTIFICATION_INVALID_STATE if the cycle's notification state is neither failed nor an eligible unsent none.
8.28 POST /rollover/cycles/:publicId/items/:studentPublicId/correct
Purpose: Repair a placement or graduation after processing has already run — the only way to change a placed/graduated item's outcome, since those statuses are otherwise terminal. One route handles all three corrective actions via the action field of RolloverCorrectionDto: "move" (relocate an active placed item to a different class/grade), "realign" (bring an item's status back in line with its current decision — see Backend §6.9 for the full case table), and "relink" (repoint a placed item's stale enrollment link at the pupil's current active enrollment, without changing status).
Body: RolloverCorrectionDto — { action: "move" | "realign" | "relink", targetClassPublicId?: string (UUID), reason: string (3–1000 chars) }.
Response: RolloverCorrectionResultDto — { item: RolloverItemDto | null }, message "Placement corrected.".
Auth: RolloverCorrections_UPDATE (superadmin-only by default).
Side Effects: each action follows the exact write sequence documented per-case in Backend §6.9 — always inside one transaction, always through ClassEnrollmentsService.enroll for any new placement, never a raw insert. A rollover.placement_changed notification is queued for the item's guardians when the correction leaves the item with an enrollment and the cycle is not still preparing/processed.
Error Cases: 409 ROLLOVER_INVALID_STATE if the cycle isn't processed/activated/finalized or has a run in progress; 404 ROLLOVER_ITEM_NOT_FOUND if the pupil has no item in this cycle; 409 ROLLOVER_CORRECTION_INVALID if the item's current state doesn't support the requested action; 409 ROLLOVER_CLASS_INVALID if the target class is missing, inactive, or in the wrong session; 409 ROLLOVER_CORRECTION_GRADE_MISMATCH (move only) if the target class's grade doesn't match the current decision; 409 ROLLOVER_ITEM_LINK_STALE (move only) if the item's current enrollment is no longer active — use relink first; 409 ACADEMIC_SESSION_LOCKED if the target session has since been locked; 404 ACADEMIC_SESSION_NOT_FOUND if the cycle's target session is missing.
8.29 GET /rollover/settings
Purpose: Read the school's rollover strategy and policy defaults.
Auth: RolloverSettings_READ (superadmin-only by default). Response: RolloverSettingsDto — see §6.15.
8.30 PATCH /rollover/settings
Purpose: Update rollover strategy and policy defaults.
Auth: RolloverSettings_UPDATE (superadmin-only by default). Body: UpdateRolloverSettingsDto, version required.
{
"defaultSectionStrategy": "keep_previous",
"capacityPolicy": "allow_override",
"version": 4
}Error Cases: 409 ROLLOVER_SETTINGS_STALE if version doesn't match the current row. The version is compared inside the UPDATE itself, so of two saves loaded at the same version exactly one succeeds.
8.31 GET /rollover/cycles/:publicId/events
Purpose: The full, newest-first, append-only audit timeline for a cycle.
Auth: Rollover_READ. Response: paginated array of RolloverEventDto (eventType, actorUserId, fromValue, toValue, reason, createdAt), one of the 29 event types listed in Backend §5.2.
8.32 GET /rollover/cycles/:publicId/integrity
Purpose: On-demand cross-check between rollover's own item bookkeeping and the live enrollment/graduation tables — the screen an admin checks if something looks inconsistent.
Auth: Rollover_READ. Response: array of findings, each carrying one of the 8 integrity codes in Backend §13 (DUPLICATE_ACTIVE_PLACEMENT, ITEM_LINK_STALE, DECISION_CHANGED_AFTER_PLACEMENT, ROLL_CONFLICT, SOURCE_SNAPSHOT_DRIFT, GRADUATE_WITH_PLACEMENT, ORPHAN_ITEM, ACTIVE_STUDENT_WITHOUT_ITEM).
8.33 GET /rollover/cycles/:publicId/id-cards
Purpose: Count of placed pupils who already have (or still need) an ID card for the target session — feeds the idCardReminder nudge.
Auth: Rollover_READ. Response: {needed: number, have: number}.
8.34 GET /rollover/cycles/:publicId/new-admissions
Purpose: List pupils who are active in the target session but were never part of this rollover cycle's roster — new admissions the admin should be aware of separately.
Auth: Rollover_READ. Response: paginated array of pupil references.
9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence — process
9.3 Error Branch — activation
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Filters | Notes |
|---|---|---|---|
GET /rollover/cycles | Standard offset (PaginationUtil) | status | Default/max sizes follow the shared pagination utility. |
GET /rollover/cycles/:publicId/items | Standard offset | view, outcome, status, exceptionCode, class/grade public IDs | view presets combine several filters at once. |
GET /rollover/cycles/:publicId/events | Standard offset, newest-first | None beyond cycle scope | Ordered by created_at descending. |
GET /rollover/cycles/:publicId/new-admissions | Standard offset | None beyond cycle scope | — |
No rollover endpoint supports pagination=false or cursor pagination; all lists use the shared offset utility with its default 20/max 100 page-size bounds.
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | No | Rollover reads Postgres directly on every request. | — |
| BullMQ | Yes | QueueName.ACADEMIC_YEAR, jobs PROCESS_ROLLOVER, ROLLOVER_RECOVERY_SWEEP, PUBLISH_PROMOTION_DECISIONS, NOTIFY_PLACEMENTS. Concurrency 1. | academic-year-queue.processor.ts, packages/jobs/src/index.ts |
| External API | No | — | — |
See Backend §9 for the full queue/job table.
13. Mandatory Deep API Documentation Pack
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Admin panel | Fingerprint/proposal-token replay requirement, superadmin-only routes, display-status computation | Show ROLLOVER_PREVIEW_STALE/ROLLOVER_PROPOSAL_STALE as "please re-review" rather than a generic error; poll /progress rather than assuming synchronous completion | Stable |
| QA | Every superadmin-only route needs a superadmin session to test meaningfully; a plain admin session against them should always return 403 | Reproduce exceptions via POST /rollover/cycles/:publicId/sync against seeded promotion decisions | Stable |
| Internal service (worker) | Not directly callable — only reachable through the academic-year queue | N/A | Stable |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| Preview/apply with a replay token, for both allocation and processing | Two-step commit everywhere consequential | Single-step "just do it" endpoints | Prevents committing against a plan that has since gone stale | Extra round trip for the client | Token is a single required field, not a complex handshake |
Async processing polled via /progress | Non-blocking for large rosters | Long-lived synchronous request | Avoids HTTP timeouts on large schools | Client must implement polling | /progress is cheap — a read of counts, no heavy computation |
| Superadmin-only defaults, reconfigurable per school | One documented, safe default | Hard admin/superadmin split with no override | Schools with a flatter hierarchy aren't forced to route everything through one account | A school could misconfigure by granting too widely | Documented explicitly in the permission catalog as a deliberate, reversible choice |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? |
|---|---|---|---|---|
| Adding a new exception/outcome code | Admin panel (must render an unknown code gracefully) | New branch in validation/processing | None — codes are string arrays, not enums at the DB level for items | No, but the panel's code-to-label map needs the new entry |
Changing capacityPolicy semantics | Admin panel, processing | Validation and processing branch differently | None | No |
| Adding a new correction case | Admin panel corrections screen | New write-order sequence in RolloverRealignService | None | No |
14. Zero-Omission API Checklist
- Every controller route across cycles/items/allocation/processing/activation/corrections/settings/insights is documented.
- Every DTO field, enum, and default is documented.
- Every auth guard and permission — including which are superadmin-only by default — is documented.
- Every success and error branch documented has a corresponding error code from Backend §13.
- Every database write, queue job, and notification side effect per endpoint is documented.
- Route ownership, request-sequence, and error-branch diagrams are included.
- Tradeoffs and compatibility risks are documented.
- This doc links to backend and features/flows.
15. Integration Checklist
- Every route from the eight controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every error code relevant to this surface is documented (cross-referenced to Backend §13).
- Every auth guard and permission is documented, including superadmin-only defaults.
- Every queue job and its trigger is documented.
- Diagrams match the current code.
- Links to backend and features/flows are present.
See Also
- Backend doc: /docs/developer/rollover/backend
- Features and flows doc: /docs/developer/rollover/feature
- Promotions: /docs/developer/promotions/api
Academic Year Rollover Backend Documentation
Backend architecture, data model, services, queues, runtime rules, and operational behavior for Academic Year Rollover.
Data Transfer Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Data Transfer.