Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Academic Year Rollover.
Source Type Files or Docs What Was Extracted Backend rollover/cycles, rollover/items, rollover/allocation, rollover/processing, rollover/activation, rollover/corrections, rollover/settings, rollover/insights, rollover/shared, rollover/workersLifecycle, validation, algorithms, worker behavior Schema packages/db/src/schema/school/rollover.ts, academic-sessions.tsTables, enums, constraints, lock columns Migrations packages/db/src/migrations/0035_promotions_rollover.sql, 0036_promotions_rollover_guards.sqlAppend-only trigger, session lock trigger Sibling module academic-sessions.service.ts, enrollments-core/class-enrollments.service.ts, shared/enrollment-removal-guards.tsswitchCurrent, enrollment writes and removal guardsSibling docs promotions/feature.mdx, promotions/backend.mdxPromotion decisions rollover consumes
Field Value Module RolloverSubmodule Cycles, Items, Allocation, Processing, Activation, Corrections, Settings, Insights Primary user value Move every pupil from one academic session to the next — placed into a class and section, or graduated — in one supervised, auditable operation instead of a term of manual re-enrollment. Actors Admin, superadmin, worker (BullMQ), scheduler Main entry points Admin panel rollover screens, POST /rollover/cycles/:publicId/process, the academic-year queue processor, the academic-year.scheduler cron Main outputs student_class_enrollments rows in the target session, student_graduations rows, rollover_events timeline, admin notices, guardian/pupil placement notificationsRelated docs Backend , API , Promotions
Rollover picks up where promotions leaves off: a promotion cycle decides, per pupil, whether they are promoted, retained, or excluded from the next session's roster. Rollover reads those finalized decisions and turns them into real class placements or graduation records in a new academic session.
Actor Can Do Cannot Do Auth Requirement Notes Admin Create/cancel a cycle, sync items, review, run section and roll allocation previews and applies, edit target class/roll on individual pupils, exclude/include pupils manually, view items/exceptions/events/integrity/insights, read and update settings Run processing, override capacity, correct placements after processing, activate/finalize/lock a year Admin JWT + Rollover_* codes Most of the cycle workflow is ordinary admin work. Superadmin Everything an admin can do, plus start processing, override section capacity, correct/realign/withdraw placements, activate the target year as current, finalize a cycle, lock/unlock a historical session, change rollover settings Nothing — this is the ceiling of the module Admin JWT + RolloverProcessing/RolloverOverrides/RolloverCorrections/RolloverActivation/AcademicYearLock/RolloverSettings codes (all superadmin-only by default) These codes publish outcomes to every family, move the whole school into a new year, or rewrite already-published history — see Backend §11 . A school can grant them down to a custom role; the default is superadmin-only. Guardian/pupil Receive a placement notification after activation See any rollover screen, cycle, or item N/A — notification only, no API surface Delivered through the existing notification channels (in_app, push, email, sms per settings), never through a rollover-specific endpoint. Worker/system Process items in leased chunks, retry on lease loss, recover stalled runs, re-enqueue stuck notifications, emit admin notices Change settings, bypass session locks, act without an actor identity Internal queue (academic-year BullMQ queue), no HTTP surface Runs as a synthetic actor (sessionId: "worker:rollover") so audit entries are distinguishable from a live admin session.
Capability Surface Actor Route/Trigger State Read State Written Linked API Section View rollover overview Admin Admin GET /rollover/overviewCurrent/target session, active cycle, decision counts None API §8.1 Start a cycle Admin Admin POST /rollover/cyclesSessions, promotion cycle rollover_cycles row (status preparing), items syncedAPI §8.3 List/inspect cycles Admin Admin GET /rollover/cycles, GET /rollover/cycles/:publicIdrollover_cycles, readiness checksNone API §8.2, §8.4 Cancel a cycle Admin Admin DELETE /rollover/cycles/:publicIdCycle status status = cancelledAPI §8.5 Sync roster Admin Admin POST /rollover/cycles/:publicId/syncPromotion decisions, enrollments rollover_items inserted/updatedAPI §8.6 Prepare review / fingerprint Admin Admin POST /rollover/cycles/:publicId/reviewItems, classes, settings, sessions Nothing — returns the fingerprint to the client API §8.7 List/filter items, exceptions Admin Admin GET /rollover/cycles/:publicId/items, /exceptionsrollover_itemsNone API §8.8–8.9 Edit target class / roll Admin Admin PATCH /rollover/cycles/:publicId/items/:studentPublicIdItem, target class Item target fields API §8.10 Bulk assign class Admin Admin POST /rollover/cycles/:publicId/items/assign-classItems, target class Item target fields API §8.11 Exclude / include a pupil Admin Admin POST /rollover/cycles/:publicId/items/exclude, /includeItem inclusion, exclusionSource, exclusionReasonAPI §8.12–8.13 Override section capacity Admin Superadmin POST /rollover/cycles/:publicId/items/capacity-overrideItem capacityOverride, overrideReasonAPI §8.14 Preview/apply section allocation Admin Admin POST /rollover/cycles/:publicId/section-allocation/{preview,apply}Target classes, items targetClassId on itemsAPI §8.16–8.17 Preview/apply roll allocation Admin Admin POST /rollover/cycles/:publicId/roll-allocation/{preview,apply}Target classes, items plannedRollNumber on itemsAPI §8.18–8.19 Start processing Admin Superadmin POST /rollover/cycles/:publicId/processConfirmed fingerprint Enqueues academic-year job API §8.20 Poll progress Admin Admin GET /rollover/cycles/:publicId/progressCycle run state, item counts None API §8.21 Preflight activation Admin Admin GET /rollover/cycles/:publicId/activation-preflightCycle, blocking/warning codes None API §8.22 Activate target year Admin Superadmin POST /rollover/cycles/:publicId/activateCycle, target session academic_sessions.is_current flips, placement notifications queuedAPI §8.23 Finalize a cycle Admin Superadmin POST /rollover/cycles/:publicId/finalizeCycle status = finalized, optional source lockAPI §8.24 Retry placement notifications Admin Superadmin POST /rollover/cycles/:publicId/notify-placementsCycle placement_notification_state = queued, outbox event enqueuedAPI §8.27 Lock/unlock a session Admin Superadmin POST /rollover/sessions/:sessionPublicId/lock, /unlockSession locked_at, locked_by_user_id, lock_reasonAPI §8.25–8.26 Move / realign / relink a placement Admin Superadmin POST /rollover/cycles/:publicId/items/:studentPublicId/correctItem, enrollment, graduation Enrollment written/removed, item status, rollover_events API §8.28 Read/update settings Admin Superadmin GET/PATCH /rollover/settingsrollover_settingsStrategy/policy fields API §8.29–8.30 View events timeline Admin Admin GET /rollover/cycles/:publicId/eventsrollover_eventsNone API §8.31 View integrity report Admin Admin GET /rollover/cycles/:publicId/integrityItems, enrollments, graduations None API §8.32 View ID-card readiness Admin Admin GET /rollover/cycles/:publicId/id-cardsPlaced items, ID card records None API §8.33 View new admissions Admin Admin GET /rollover/cycles/:publicId/new-admissionsTarget session enrollments not in the cycle None API §8.34 Recover a stalled run Worker System academic-year.scheduler cron every 5 minutesCycle lease state Lease cleared, job re-enqueued or run marked failed Backend §9 Send placement notifications Worker System NOTIFY_PLACEMENTS job, after activationPlaced items placementNotifiedAt, notification rowsBackend §9
An admin opens the rollover screen for a source session whose promotion cycle is finalized, starts a new rollover cycle against a target session, and the roster of eligible pupils is synced from the promotion decisions automatically.
The promotion cycle for the source session is finalized (ROLLOVER_PROMOTION_NOT_FINALIZED otherwise).
Source and target sessions are distinct and the target starts after the source (ROLLOVER_SAME_SESSION, ROLLOVER_TARGET_BEFORE_SOURCE).
The target session is active and unlocked (ROLLOVER_TARGET_INACTIVE, ROLLOVER_TARGET_LOCKED).
No other live cycle exists for the same source/target/promotion-cycle triple (ROLLOVER_CYCLE_EXISTS).
Step Actor/System Action Result Source 1 Admin POST /rollover/cycles with source/target session and promotion cycleCycle row created, status = preparing rollover-cycles.service.ts2 Backend Sync roster from promotion decisions One rollover_items row per eligible pupil (see §5.2 ) rollover-sync.service.ts3 Admin Review readiness checklist (GET /rollover/cycles/:publicId) 15 readiness checks across 7 areas, each ready/warning/error/not_applicable/pending rollover-readiness.service.ts
Branch Condition Behavior Error/Result Duplicate cycle A live cycle already targets this session pair Reject creation ROLLOVER_CYCLE_EXISTSTarget locked Target session has locked_at set Reject creation ROLLOVER_TARGET_LOCKEDPromotion cycle not finalized Source promotion cycle still in progress Reject creation ROLLOVER_PROMOTION_NOT_FINALIZEDNew admission after sync A pupil enrolls in the source session after the roster was synced Re-sync (POST /rollover/cycles/:publicId/sync) picks it up as a new item Item inserted, auto-excluded if their decision outcome is excluded
Rollover never trusts a snapshot taken once — an admin can re-run sync at any time before processing to pick up changes in promotion decisions or source-session enrollment.
Cycle status is preparing or processed and runState is idle (assertCycleMutable).
Step Actor/System Action Result Source 1 Admin POST /rollover/cycles/:publicId/syncRoster recomputed rollover-sync.service.ts2 Backend Roster = pupils with a current promotion decision and an active enrollment in the source session New pupils inserted, existing non-terminal items updated rollover-sync.service.ts:51-1923 Backend New item whose decision outcome is excluded Auto-excluded with reason AUTO_EXCLUSION_REASON rollover-sync.service.ts4 Backend Existing item whose target grade changed Clears targetClassId and plannedRollNumber, records event rollover-sync.service.ts
Branch Condition Behavior Error/Result Terminal item (placed/graduated/withdrawn) Sync runs again Item is left untouched No write Manual exclusion Item was manually excluded by an admin Sync never overwrites a manual exclusion with an auto-exclusion exclusionSource = manual preservedDecision changed after prior sync Promotion decision was revised currentDecisionId/outcome resynced onto the itemFingerprint changes, forcing a fresh review
Before processing, an admin previews how the pupils in a grade would be distributed across target classes (section allocation) and what roll number each would receive (roll allocation), then applies the proposal they accept.
The target grade has at least one active class (TARGET_GRADE_NO_CLASS otherwise).
A proposal must be applied with the same token it was previewed with, or it is rejected as stale.
Step Actor/System Action Result Source 1 Admin POST .../section-allocation/preview with grade, strategy, onlyUnassigned, preserveShiftProposal with proposalToken, per-class assignment counts, unassignedCount rollover-section-allocation.service.ts2 Admin POST .../section-allocation/apply with the same tokentargetClassId written on affected itemsrollover-section-allocation.service.ts3 Admin POST .../roll-allocation/preview with grade, strategy, scopeProposal with roll assignments and warnings rollover-roll-allocation.service.ts4 Admin POST .../roll-allocation/apply with the same tokenplannedRollNumber written on affected itemsrollover-roll-allocation.service.ts
See Backend §6.4–6.5 for the exact algorithm and worked examples.
Branch Condition Behavior Error/Result Stale proposal Cycle state changed between preview and apply Reject apply ROLLOVER_PROPOSAL_STALENo room anywhere Every eligible class is at capacity Pupil left unassigned unassignedCount includes reason CAPACITYShift-restricted pupil, no same-shift class preserveShift = true and no class shares the pupil's shiftPupil left unassigned Reason NO_CLASS_FOR_SHIFT Grade has no active class Target grade has zero eligible classes Every pupil in the grade unassigned Reason NO_CLASS
Once the admin has reviewed the roster and is satisfied, a superadmin confirms the current state (via its fingerprint) and starts background processing, which places or graduates every included pupil.
The client holds a fingerprint from POST /rollover/cycles/:publicId/review that still matches the live state.
confirm: true is sent explicitly (ROLLOVER_CONFIRMATION_REQUIRED otherwise).
No run is already in progress for this cycle.
Step Actor/System Action Result Source 1 Superadmin POST /rollover/cycles/:publicId/process with fingerprint, confirm: trueFingerprint compared against current state rollover-process-request.service.ts2 Backend Fingerprint matches Enqueue PROCESS_ROLLOVER job via outbox, attempt incremented rollover-process-request.service.ts3 Worker Acquire a 2-minute lease on the cycle runState = running, leaseToken setrollover-runner.service.ts4 Worker Process items in chunks of 50, in processOrder, inside one DB transaction per item Items become placed, graduated, needs_review, awaiting_section, or failed rollover-runner.service.ts5 Worker Renew the lease heartbeat between chunks Run continues past 2 minutes rollover-runner.service.ts6 Worker All items processed Cycle status preparing → processed, runOutcome computed, admin notice sent rollover-runner.service.ts
Branch Condition Behavior Error/Result Stale fingerprint State changed since review Reject process request ROLLOVER_PREVIEW_STALE (409)Nothing to process No non-terminal included items Reject ROLLOVER_NOTHING_TO_PROCESSWorker crash mid-run Lease expires without heartbeat Sweep re-enqueues the run, recoveryCount incremented See §5.5 Item-level failure Unhandled error inside one item's transaction That item marked failed with UNEXPECTED_ERROR, run continues to the next item rollover-runner.service.tsItem at capacity, no override Section is full and capacityOverride is not set Item marked with SECTION_CAPACITY exception, held for review rollover-validation.service.tsSerialization conflict Concurrent write collides (deadlock/serialization failure) Whole item transaction retried once automatically with-lock-retry.ts
If a worker dies mid-run or a job is lost before a worker ever picks it up, a scheduled sweep detects and recovers it automatically, without any admin action.
Step Actor/System Action Result Source 1 Scheduler Every 5 minutes, enqueue a ROLLOVER_RECOVERY_SWEEP job Sweep runs academic-year.scheduler.ts2 Sweep Find cycles runState = running whose lease expired, or whose job was never claimed for 3+ minutes Candidate list academic-year-sweep.service.ts3 Sweep Clear the dead lease, bump recoveryCount, re-enqueue with a deduped job id Run resumes from the next unprocessed item academic-year-sweep.service.ts4 Sweep recoveryCount exceeds 5Run marked runOutcome = partially_completed, failureCode = RECOVERY_EXHAUSTED, admin notified academic-year-sweep.service.ts
Branch Condition Behavior Error/Result Recoverable stall Recovery count ≤ 5 Job re-enqueued, run continues transparently No admin action needed Recovery exhausted 6th stall detected Run stopped permanently, requires manual re-process RECOVERY_EXHAUSTED admin noticeNotification stuck in queued A promotion publication or placement notification hasn't advanced in 10 minutes Re-enqueued, up to 5 attempts academic-year-sweep.service.tsNotification attempts exhausted 5th stuck attempt Marked failed, no further retry academic-year-sweep.service.ts
Route: POST /rollover/cycles/:publicId/items/exclude and /include. Permission Rollover_UPDATE. Excluding sets inclusion = excluded, exclusionSource = manual, and requires a reason; including clears those fields. Recorded as item_excluded/item_included events. Never allowed on a terminal item.
Route: POST /rollover/cycles/:publicId/items/capacity-override. Permission RolloverOverrides_UPDATE (superadmin-only). Marks an item so validation and processing may place it in a class already at capacity, with a mandatory overrideReason. Recorded as a capacity_override event.
Corrections operate on already-placed or already-graduated items and are superadmin-only (RolloverCorrections). Every case runs inside one transaction with a fixed write order — see Backend §6.9 for the exact sequence per case (move, exclude, graduate, revoke).
Route: POST /rollover/cycles/:publicId/activate, permission RolloverActivation (superadmin-only). Runs a preflight check first (blocking codes like RUN_IN_PROGRESS, NOT_PROCESSED, TARGET_LOCKED must all be clear), then calls academic-sessions.service.ts's switchCurrent with via: "rollover" so the in-flight-cycle check that would otherwise block a manual session switch is skipped — the rollover cycle itself is the reason the switch is happening. Activation also enqueues placement notifications. Finalize marks the cycle finalized and optionally locks the source session.
Route: POST /rollover/sessions/:sessionPublicId/lock, /unlock. Permission AcademicYearLock (superadmin-only). Locking sets locked_at, locked_by_user_id, lock_reason on academic_sessions; the database itself then refuses any insert/update/delete against that session's enrollments and classes via a trigger (see Backend §5.4 ), independent of any application-layer check.
Entity From Event/Action To Guard Condition Side Effects Cycle (none) Create preparingSource/target valid, no duplicate cycle Roster synced Cycle preparingProcess completes processedRun finished without a forced failure runOutcome setCycle processedActivate activatedPreflight passes, RolloverActivation academic_sessions.is_current flips, notifications queuedCycle activatedFinalize finalizedRolloverActivation/finalize permissionOptional source session lock Cycle preparing or processedCancel cancelledNo irreversible placements yet made No further mutation allowed
Cycle display status (computed, not stored) additionally shows processing, completed, completed_with_exceptions, and partially_completed, derived from runState and runOutcome — see Backend §5.2 .
Entity From Event/Action To Guard Condition Side Effects Item (none) Sync pendingPupil has a current decision and active source enrollment — Item pendingProcessing places the pupil placedClass resolved, capacity available or overridden Enrollment created Item pendingProcessing graduates the pupil graduatedOutcome is a graduating outcome student_graduations row createdItem pendingMissing target class awaiting_sectionNo targetClassId set yet — Item pendingValidation finds a blocking exception needs_reviewAny exception code present Held from processing Item pending/needs_reviewUnexpected error during processing failedUncaught error in item transaction outcomeCode = UNEXPECTED_ERROR, attempts incrementedItem placed/graduatedManual exclusion via correction withdrawnRolloverCorrectionsEnrollment removed / graduation revoked
placed, graduated, withdrawn are terminal — sync and re-validation never touch them again.
Flow DB Writes Cache Effects Jobs Realtime Analytics Notifications Create cycle rollover_cycles, rollover_itemsNone None None None None Sync rollover_items, rollover_eventsNone None None None None Review None (read-only) None None None None None Allocation apply rollover_items.targetClassId/plannedRollNumberNone None None None None Process rollover_items, student_class_enrollments, student_graduations, rollover_events, rollover_cyclesNone academic-year queue (PROCESS_ROLLOVER, NOTIFY_PLACEMENTS)None None Admin notice on finish/failure Activate academic_sessions.is_current, rollover_cyclesNone Enqueues NOTIFY_PLACEMENTS if placements pending None None Placement notifications to guardians/pupils Correction student_class_enrollments, student_graduations, rollover_items, rollover_eventsNone None None None None Lock session academic_sessionsNone None None None None
Scenario Trigger User/System Experience Recovery Source Stale fingerprint at process time State changed since last review 409 ROLLOVER_PREVIEW_STALERe-run review to get a fresh fingerprint rollover-process-request.service.tsStale proposal at allocation apply Cycle state changed since preview 409 ROLLOVER_PROPOSAL_STALERe-preview and re-apply rollover-section-allocation.service.tsSession locked mid-write A trigger-guarded write hits a locked session 409 ACADEMIC_SESSION_LOCKEDUnlock the session, or target a different one all-exceptions.filter.ts, 0036_promotions_rollover_guards.sqlWorker lease lost Heartbeat write fails because another attempt claimed the lease Run stops writing, returns lease_lost Sweep re-enqueues automatically rollover-runner.service.tsRecovery exhausted 6th stall on the same run Cycle marked partially_completed, RECOVERY_EXHAUSTED Superadmin re-runs processing manually academic-year-sweep.service.tsConcurrent write conflict Serialization failure or deadlock inside an item transaction Transparent — one automatic retry None needed unless retry also fails with-lock-retry.tsEnrollment removal blocked Attempt to delete an enrollment linked to a rollover item, or in a locked session 409 ENROLLMENT_LINKED_TO_ROLLOVER / ACADEMIC_SESSION_LOCKEDUse a rollover correction instead of a direct delete enrollment-removal-guards.ts
Covered above: actor capability matrix (§3 ), high-level flow diagrams (§5 ), state machines (§7 ), correction activity diagram (§6.3 ), data side-effect table (§9 ).
Feature Minor Behavior Actor Trigger User/System Result Backend Side Effect Source Sync Re-sync is idempotent for terminal items Admin POST /rollover/cycles/:publicId/syncPlaced/graduated/withdrawn pupils unaffected No write to terminal items rollover-sync.service.tsSync Auto-exclusion reason is fixed text System New item with outcome = excluded Item excluded automatically exclusionReason = "The current promotion decision outcome is excluded."rollover-sync.service.tsSection allocation Shift-restricted fallback System preserveShift = true, no same-shift class existsStudent becomes eligible for any shift instead of being stuck unassigned shiftRestricted: false returnedsection-allocation.util.ts:117-129Roll allocation Roll cap System Any strategy No roll ever exceeds 999 nextFree returns null past 999, item heldroll-allocation.util.ts:18Roll allocation Grade-scope reservation System scope = gradeA roll used in one class of the grade cannot repeat in a sibling class Reserved set spans all classes in the grade roll-allocation.util.tsProcessing SKIP LOCKED Worker Concurrent admin correction on an item mid-run Corrector can lock a row a running processor hasn't reached yet without deadlocking FOR UPDATE SKIP LOCKED on the chunk fetchrollover-runner.service.tsProcessing Deferred effects flushed after commit Worker Every processed item Cache/audit side effects never fire for a rolled-back item Effects buffered per item, flushed only post-commit rollover-runner.service.tsActivation via: "rollover" bypassSuperadmin Activate The generic in-flight-cycle guard on switchCurrent doesn't self-block the very cycle driving the switch academic-sessions.service.ts skips its own cycle check for this calleracademic-sessions.service.ts:376-389Corrections Append-only events System Any correction The event history for a cycle can never be edited or deleted, even by a later correction rollover_events_append_only trigger raises on UPDATE/DELETE outside a cascade0036_promotions_rollover_guards.sql:131-143Notifications No PII in push/SMS Worker Placement notification Guardian sees a generic "an academic update is available" push, opens the app for detail promotion-templates.ts withholds student name, marks and reasons from push/SMS bodiespromotion-templates.ts:168-202Sessions Lock blocks cascades except in a cascade System Delete a student in a locked session The lock still permits deleting the pupil themselves (cascade), just not editing their locked-year records directly Trigger early-returns when pg_trigger_depth() > 1 0036_promotions_rollover_guards.sql:155-188
Rule Business Reason Actor Impact Enforced In API Impact Backend Impact Tests A cycle cannot be processed with a stale fingerprint Prevents placing pupils against a roster the admin never actually reviewed Admin must re-review after any change rollover-process-request.service.ts409 ROLLOVER_PREVIEW_STALEFingerprint recomputed and compared before enqueue rollover/shared/__tests__Processing is superadmin-only Placement is irreversible enough to publish to every family; only the highest trust tier starts it Ordinary admins prepare, superadmin commits Permission catalog RolloverProcessing 403 without the code— permission-catalog.spec.tsPlacement notifications never fire before activation A pupil isn't really "in" the new class until the year is actually switched Guardians never see a notification for a placement that could still be corrected placement-notification.service.tsNotification only enqueued at activation NOTIFY_PLACEMENTS conditioned on activated/finalizedrollover/shared/__tests__A locked session refuses enrollment/class writes at the database level History that has already been activated/finalized cannot be silently rewritten Any attempt to touch a locked year's records fails, even outside this module 0036_promotions_rollover_guards.sql trigger409 ACADEMIC_SESSION_LOCKEDEnforced independent of any service-layer check academic-sessions specsSection capacity is counted, not estimated Prevents overbooking a physical classroom Admin sees an accurate SECTION_CAPACITY exception, not a surprise after the fact rollover-validation.service.ts:310-335Exception surfaces in /exceptions used + admitted < capacity, in processOrderrollover-validation.service.ts specsManual exclusion always wins over auto-exclusion An admin's deliberate decision must not be silently reverted by a routine re-sync Admin can safely re-sync without losing manual work rollover-sync.service.ts— exclusionSource = manual is never overwrittenrollover-sync.service.ts specs
Product Decision User Benefit Engineering Benefit Alternative Tradeoff Risk Fingerprint-gated processing instead of a simple "confirm" button Admin cannot accidentally process a stale plan No need to re-lock the cycle for review A pessimistic lock held from review to process A lock would block all other admin work on the cycle for the review window A fingerprint miss forces a re-review, which can feel repetitive if changes are frequent Async chunked processing instead of one synchronous request Large schools with thousands of pupils don't time out an HTTP request Per-item transactions isolate one bad row from the whole run Synchronous bulk transaction A single failing row would either abort everything or require complex partial-commit logic Progress must be polled rather than returned immediately Superadmin-only gate on the most consequential actions by default, but reconfigurable per school Small schools aren't forced into a rigid hierarchy they don't have One documented default instead of per-school special-casing Hard-code superadmin-only with no override A school could grant these to a role that isn't ready for them Documented in the permission catalog as a deliberate reversibility tradeoff Recovery sweep instead of relying on BullMQ's own retry Recovers even a job BullMQ never got a chance to hand to a worker (worker crashed before claiming it) One codepath handles both "processed and died" and "never even started" Rely solely on BullMQ attempt/backoff Requires its own polling cadence and cap (5 recoveries) A genuinely broken run could loop through recoveries before someone notices — bounded by RECOVERY_EXHAUSTED
Flow Edge Case Trigger Expected Behavior User/System Feedback Source Sync Empty roster No pupil has an active source enrollment Sync returns {inserted: 0, updated: 0} Cycle has zero items rollover-sync.service.tsSection allocation 85 into two 40-capacity sections balanced strategy40 assigned to each, 5 left unassignedCount with reason CAPACITY See Backend §6.4 worked example section-allocation.util.tsRoll allocation 60 into three sections, section scope alphabetical strategyEach section independently numbered 1–20 See Backend §6.5 worked example roll-allocation.util.tsProcessing Duplicate placement attempt Item already has a live enrollment when processed again DUPLICATE_PLACEMENT exception, item heldSurfaced under /exceptions rollover-validation.service.tsProcessing Concurrent action Two admins correct the same item while a run is in progress SKIP LOCKED lets the corrector proceed on a not-yet-processed row; a row already locked by the runner is skipped by the corrector until releasedNo deadlock rollover-runner.service.tsProcessing Expired lease Worker crashes mid-chunk Lease expires after 2 minutes, sweep recovers within its 5-minute cadence Admin sees runState = running briefly continue via the recovered attempt academic-year-sweep.service.tsActivation Permission mismatch Admin (not superadmin) calls activate 403No state change Permission catalog Corrections Missing dependency Correction targets an item with no source enrollment reference ROLLOVER_CORRECTION_INVALIDCorrection rejected rollover-corrections.service.tsAllocation Unsupported filter Query an item list with an unrecognized view value Validation rejects the request 400rollover-item.dto.tsSessions Queue failure equivalent — outbox never dispatched Outbox row written but dispatcher hasn't relayed it yet Job simply hasn't started; the sweep only intervenes once a run is genuinely stalled No user-visible error academic-year-sweep.service.ts
Flow Reads Writes Cache Jobs/Events Response Fields Process rollover_cycles, rollover_items, rollover_settingsrollover_cycles, rollover_items, student_class_enrollments, student_graduations, rollover_eventsNone academic-year queue (PROCESS_ROLLOVER)attempt, itemsToRunProgress rollover_cycles, rollover_items (counts)None None None displayStatus, runState, runOutcome, counts
The doc explains what the actor is trying to accomplish — move a school's pupils into a new academic year in one supervised operation.
The doc explains backend behavior the actor does not see — leasing, chunking, the recovery sweep, the append-only event trigger.
The doc covers minor flows and branches — auto-exclusion, shift fallback, roll caps, SKIP LOCKED.
The doc includes admin, superadmin, worker, and system flows.
The doc explains business logic, tradeoffs, and rationale.
The doc maps flows to API and backend sections.
The doc includes diagrams for each flow type present.
The doc covers edge cases and failure recovery.