Skoolsewa - School Management System Docs
Developer ResourcesAcademic year rollover

Academic Year Rollover Features and Flows

Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Academic Year Rollover.

Academic Year Rollover Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Backendrollover/cycles, rollover/items, rollover/allocation, rollover/processing, rollover/activation, rollover/corrections, rollover/settings, rollover/insights, rollover/shared, rollover/workersLifecycle, validation, algorithms, worker behavior
Schemapackages/db/src/schema/school/rollover.ts, academic-sessions.tsTables, enums, constraints, lock columns
Migrationspackages/db/src/migrations/0035_promotions_rollover.sql, 0036_promotions_rollover_guards.sqlAppend-only trigger, session lock trigger
Sibling moduleacademic-sessions.service.ts, enrollments-core/class-enrollments.service.ts, shared/enrollment-removal-guards.tsswitchCurrent, enrollment writes and removal guards
Sibling docspromotions/feature.mdx, promotions/backend.mdxPromotion decisions rollover consumes

2. Feature Summary

FieldValue
ModuleRollover
SubmoduleCycles, Items, Allocation, Processing, Activation, Corrections, Settings, Insights
Primary user valueMove 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.
ActorsAdmin, superadmin, worker (BullMQ), scheduler
Main entry pointsAdmin panel rollover screens, POST /rollover/cycles/:publicId/process, the academic-year queue processor, the academic-year.scheduler cron
Main outputsstudent_class_enrollments rows in the target session, student_graduations rows, rollover_events timeline, admin notices, guardian/pupil placement notifications
Related docsBackend, 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.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
AdminCreate/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 settingsRun processing, override capacity, correct placements after processing, activate/finalize/lock a yearAdmin JWT + Rollover_* codesMost of the cycle workflow is ordinary admin work.
SuperadminEverything 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 settingsNothing — this is the ceiling of the moduleAdmin 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/pupilReceive a placement notification after activationSee any rollover screen, cycle, or itemN/A — notification only, no API surfaceDelivered through the existing notification channels (in_app, push, email, sms per settings), never through a rollover-specific endpoint.
Worker/systemProcess items in leased chunks, retry on lease loss, recover stalled runs, re-enqueue stuck notifications, emit admin noticesChange settings, bypass session locks, act without an actor identityInternal queue (academic-year BullMQ queue), no HTTP surfaceRuns as a synthetic actor (sessionId: "worker:rollover") so audit entries are distinguishable from a live admin session.

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
View rollover overviewAdminAdminGET /rollover/overviewCurrent/target session, active cycle, decision countsNoneAPI §8.1
Start a cycleAdminAdminPOST /rollover/cyclesSessions, promotion cyclerollover_cycles row (status preparing), items syncedAPI §8.3
List/inspect cyclesAdminAdminGET /rollover/cycles, GET /rollover/cycles/:publicIdrollover_cycles, readiness checksNoneAPI §8.2, §8.4
Cancel a cycleAdminAdminDELETE /rollover/cycles/:publicIdCycle statusstatus = cancelledAPI §8.5
Sync rosterAdminAdminPOST /rollover/cycles/:publicId/syncPromotion decisions, enrollmentsrollover_items inserted/updatedAPI §8.6
Prepare review / fingerprintAdminAdminPOST /rollover/cycles/:publicId/reviewItems, classes, settings, sessionsNothing — returns the fingerprint to the clientAPI §8.7
List/filter items, exceptionsAdminAdminGET /rollover/cycles/:publicId/items, /exceptionsrollover_itemsNoneAPI §8.8–8.9
Edit target class / rollAdminAdminPATCH /rollover/cycles/:publicId/items/:studentPublicIdItem, target classItem target fieldsAPI §8.10
Bulk assign classAdminAdminPOST /rollover/cycles/:publicId/items/assign-classItems, target classItem target fieldsAPI §8.11
Exclude / include a pupilAdminAdminPOST /rollover/cycles/:publicId/items/exclude, /includeIteminclusion, exclusionSource, exclusionReasonAPI §8.12–8.13
Override section capacityAdminSuperadminPOST /rollover/cycles/:publicId/items/capacity-overrideItemcapacityOverride, overrideReasonAPI §8.14
Preview/apply section allocationAdminAdminPOST /rollover/cycles/:publicId/section-allocation/{preview,apply}Target classes, itemstargetClassId on itemsAPI §8.16–8.17
Preview/apply roll allocationAdminAdminPOST /rollover/cycles/:publicId/roll-allocation/{preview,apply}Target classes, itemsplannedRollNumber on itemsAPI §8.18–8.19
Start processingAdminSuperadminPOST /rollover/cycles/:publicId/processConfirmed fingerprintEnqueues academic-year jobAPI §8.20
Poll progressAdminAdminGET /rollover/cycles/:publicId/progressCycle run state, item countsNoneAPI §8.21
Preflight activationAdminAdminGET /rollover/cycles/:publicId/activation-preflightCycle, blocking/warning codesNoneAPI §8.22
Activate target yearAdminSuperadminPOST /rollover/cycles/:publicId/activateCycle, target sessionacademic_sessions.is_current flips, placement notifications queuedAPI §8.23
Finalize a cycleAdminSuperadminPOST /rollover/cycles/:publicId/finalizeCyclestatus = finalized, optional source lockAPI §8.24
Retry placement notificationsAdminSuperadminPOST /rollover/cycles/:publicId/notify-placementsCycleplacement_notification_state = queued, outbox event enqueuedAPI §8.27
Lock/unlock a sessionAdminSuperadminPOST /rollover/sessions/:sessionPublicId/lock, /unlockSessionlocked_at, locked_by_user_id, lock_reasonAPI §8.25–8.26
Move / realign / relink a placementAdminSuperadminPOST /rollover/cycles/:publicId/items/:studentPublicId/correctItem, enrollment, graduationEnrollment written/removed, item status, rollover_eventsAPI §8.28
Read/update settingsAdminSuperadminGET/PATCH /rollover/settingsrollover_settingsStrategy/policy fieldsAPI §8.29–8.30
View events timelineAdminAdminGET /rollover/cycles/:publicId/eventsrollover_eventsNoneAPI §8.31
View integrity reportAdminAdminGET /rollover/cycles/:publicId/integrityItems, enrollments, graduationsNoneAPI §8.32
View ID-card readinessAdminAdminGET /rollover/cycles/:publicId/id-cardsPlaced items, ID card recordsNoneAPI §8.33
View new admissionsAdminAdminGET /rollover/cycles/:publicId/new-admissionsTarget session enrollments not in the cycleNoneAPI §8.34
Recover a stalled runWorkerSystemacademic-year.scheduler cron every 5 minutesCycle lease stateLease cleared, job re-enqueued or run marked failedBackend §9
Send placement notificationsWorkerSystemNOTIFY_PLACEMENTS job, after activationPlaced itemsplacementNotifiedAt, notification rowsBackend §9

5. User-Facing Flows

5.1 Prepare a rollover cycle

Summary

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.

Preconditions

  • 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).

Main Flow

StepActor/SystemActionResultSource
1AdminPOST /rollover/cycles with source/target session and promotion cycleCycle row created, status = preparingrollover-cycles.service.ts
2BackendSync roster from promotion decisionsOne rollover_items row per eligible pupil (see §5.2)rollover-sync.service.ts
3AdminReview readiness checklist (GET /rollover/cycles/:publicId)15 readiness checks across 7 areas, each ready/warning/error/not_applicable/pendingrollover-readiness.service.ts

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Duplicate cycleA live cycle already targets this session pairReject creationROLLOVER_CYCLE_EXISTS
Target lockedTarget session has locked_at setReject creationROLLOVER_TARGET_LOCKED
Promotion cycle not finalizedSource promotion cycle still in progressReject creationROLLOVER_PROMOTION_NOT_FINALIZED
New admission after syncA pupil enrolls in the source session after the roster was syncedRe-sync (POST /rollover/cycles/:publicId/sync) picks it up as a new itemItem inserted, auto-excluded if their decision outcome is excluded

5.2 Sync the roster

Summary

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.

Preconditions

  • Cycle status is preparing or processed and runState is idle (assertCycleMutable).

Main Flow

StepActor/SystemActionResultSource
1AdminPOST /rollover/cycles/:publicId/syncRoster recomputedrollover-sync.service.ts
2BackendRoster = pupils with a current promotion decision and an active enrollment in the source sessionNew pupils inserted, existing non-terminal items updatedrollover-sync.service.ts:51-192
3BackendNew item whose decision outcome is excludedAuto-excluded with reason AUTO_EXCLUSION_REASONrollover-sync.service.ts
4BackendExisting item whose target grade changedClears targetClassId and plannedRollNumber, records eventrollover-sync.service.ts

Branches and Edge Cases

BranchConditionBehaviorError/Result
Terminal item (placed/graduated/withdrawn)Sync runs againItem is left untouchedNo write
Manual exclusionItem was manually excluded by an adminSync never overwrites a manual exclusion with an auto-exclusionexclusionSource = manual preserved
Decision changed after prior syncPromotion decision was revisedcurrentDecisionId/outcome resynced onto the itemFingerprint changes, forcing a fresh review

5.3 Allocate sections and roll numbers

Summary

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.

Preconditions

  • 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.

Main Flow

StepActor/SystemActionResultSource
1AdminPOST .../section-allocation/preview with grade, strategy, onlyUnassigned, preserveShiftProposal with proposalToken, per-class assignment counts, unassignedCountrollover-section-allocation.service.ts
2AdminPOST .../section-allocation/apply with the same tokentargetClassId written on affected itemsrollover-section-allocation.service.ts
3AdminPOST .../roll-allocation/preview with grade, strategy, scopeProposal with roll assignments and warningsrollover-roll-allocation.service.ts
4AdminPOST .../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.

Branches and Edge Cases

BranchConditionBehaviorError/Result
Stale proposalCycle state changed between preview and applyReject applyROLLOVER_PROPOSAL_STALE
No room anywhereEvery eligible class is at capacityPupil left unassignedunassignedCount includes reason CAPACITY
Shift-restricted pupil, no same-shift classpreserveShift = true and no class shares the pupil's shiftPupil left unassignedReason NO_CLASS_FOR_SHIFT
Grade has no active classTarget grade has zero eligible classesEvery pupil in the grade unassignedReason NO_CLASS

5.4 Process a cycle

Summary

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.

Preconditions

  • 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.

Main Flow

StepActor/SystemActionResultSource
1SuperadminPOST /rollover/cycles/:publicId/process with fingerprint, confirm: trueFingerprint compared against current staterollover-process-request.service.ts
2BackendFingerprint matchesEnqueue PROCESS_ROLLOVER job via outbox, attempt incrementedrollover-process-request.service.ts
3WorkerAcquire a 2-minute lease on the cyclerunState = running, leaseToken setrollover-runner.service.ts
4WorkerProcess items in chunks of 50, in processOrder, inside one DB transaction per itemItems become placed, graduated, needs_review, awaiting_section, or failedrollover-runner.service.ts
5WorkerRenew the lease heartbeat between chunksRun continues past 2 minutesrollover-runner.service.ts
6WorkerAll items processedCycle status preparing → processed, runOutcome computed, admin notice sentrollover-runner.service.ts

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Stale fingerprintState changed since reviewReject process requestROLLOVER_PREVIEW_STALE (409)
Nothing to processNo non-terminal included itemsRejectROLLOVER_NOTHING_TO_PROCESS
Worker crash mid-runLease expires without heartbeatSweep re-enqueues the run, recoveryCount incrementedSee §5.5
Item-level failureUnhandled error inside one item's transactionThat item marked failed with UNEXPECTED_ERROR, run continues to the next itemrollover-runner.service.ts
Item at capacity, no overrideSection is full and capacityOverride is not setItem marked with SECTION_CAPACITY exception, held for reviewrollover-validation.service.ts
Serialization conflictConcurrent write collides (deadlock/serialization failure)Whole item transaction retried once automaticallywith-lock-retry.ts

5.5 Recover a stalled run

Summary

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.

Main Flow

StepActor/SystemActionResultSource
1SchedulerEvery 5 minutes, enqueue a ROLLOVER_RECOVERY_SWEEP jobSweep runsacademic-year.scheduler.ts
2SweepFind cycles runState = running whose lease expired, or whose job was never claimed for 3+ minutesCandidate listacademic-year-sweep.service.ts
3SweepClear the dead lease, bump recoveryCount, re-enqueue with a deduped job idRun resumes from the next unprocessed itemacademic-year-sweep.service.ts
4SweeprecoveryCount exceeds 5Run marked runOutcome = partially_completed, failureCode = RECOVERY_EXHAUSTED, admin notifiedacademic-year-sweep.service.ts

Branches and Edge Cases

BranchConditionBehaviorError/Result
Recoverable stallRecovery count ≤ 5Job re-enqueued, run continues transparentlyNo admin action needed
Recovery exhausted6th stall detectedRun stopped permanently, requires manual re-processRECOVERY_EXHAUSTED admin notice
Notification stuck in queuedA promotion publication or placement notification hasn't advanced in 10 minutesRe-enqueued, up to 5 attemptsacademic-year-sweep.service.ts
Notification attempts exhausted5th stuck attemptMarked failed, no further retryacademic-year-sweep.service.ts

6. Admin Flows

6.1 Manual exclude / include

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.

6.2 Capacity override

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.

6.3 Correction after processing

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).

6.4 Activation and finalize

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.

6.5 Lock / unlock a 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.

7. Lifecycle and State Transitions

7.1 Cycle lifecycle

EntityFromEvent/ActionToGuard ConditionSide Effects
Cycle(none)CreatepreparingSource/target valid, no duplicate cycleRoster synced
CyclepreparingProcess completesprocessedRun finished without a forced failurerunOutcome set
CycleprocessedActivateactivatedPreflight passes, RolloverActivationacademic_sessions.is_current flips, notifications queued
CycleactivatedFinalizefinalizedRolloverActivation/finalize permissionOptional source session lock
Cyclepreparing or processedCancelcancelledNo irreversible placements yet madeNo 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.

7.2 Item lifecycle

EntityFromEvent/ActionToGuard ConditionSide Effects
Item(none)SyncpendingPupil has a current decision and active source enrollment—
ItempendingProcessing places the pupilplacedClass resolved, capacity available or overriddenEnrollment created
ItempendingProcessing graduates the pupilgraduatedOutcome is a graduating outcomestudent_graduations row created
ItempendingMissing target classawaiting_sectionNo targetClassId set yet—
ItempendingValidation finds a blocking exceptionneeds_reviewAny exception code presentHeld from processing
Itempending/needs_reviewUnexpected error during processingfailedUncaught error in item transactionoutcomeCode = UNEXPECTED_ERROR, attempts incremented
Itemplaced/graduatedManual exclusion via correctionwithdrawnRolloverCorrectionsEnrollment removed / graduation revoked

placed, graduated, withdrawn are terminal — sync and re-validation never touch them again.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Create cyclerollover_cycles, rollover_itemsNoneNoneNoneNoneNone
Syncrollover_items, rollover_eventsNoneNoneNoneNoneNone
ReviewNone (read-only)NoneNoneNoneNoneNone
Allocation applyrollover_items.targetClassId/plannedRollNumberNoneNoneNoneNoneNone
Processrollover_items, student_class_enrollments, student_graduations, rollover_events, rollover_cyclesNoneacademic-year queue (PROCESS_ROLLOVER, NOTIFY_PLACEMENTS)NoneNoneAdmin notice on finish/failure
Activateacademic_sessions.is_current, rollover_cyclesNoneEnqueues NOTIFY_PLACEMENTS if placements pendingNoneNonePlacement notifications to guardians/pupils
Correctionstudent_class_enrollments, student_graduations, rollover_items, rollover_eventsNoneNoneNoneNoneNone
Lock sessionacademic_sessionsNoneNoneNoneNoneNone

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Stale fingerprint at process timeState changed since last review409 ROLLOVER_PREVIEW_STALERe-run review to get a fresh fingerprintrollover-process-request.service.ts
Stale proposal at allocation applyCycle state changed since preview409 ROLLOVER_PROPOSAL_STALERe-preview and re-applyrollover-section-allocation.service.ts
Session locked mid-writeA trigger-guarded write hits a locked session409 ACADEMIC_SESSION_LOCKEDUnlock the session, or target a different oneall-exceptions.filter.ts, 0036_promotions_rollover_guards.sql
Worker lease lostHeartbeat write fails because another attempt claimed the leaseRun stops writing, returns lease_lostSweep re-enqueues automaticallyrollover-runner.service.ts
Recovery exhausted6th stall on the same runCycle marked partially_completed, RECOVERY_EXHAUSTEDSuperadmin re-runs processing manuallyacademic-year-sweep.service.ts
Concurrent write conflictSerialization failure or deadlock inside an item transactionTransparent — one automatic retryNone needed unless retry also failswith-lock-retry.ts
Enrollment removal blockedAttempt to delete an enrollment linked to a rollover item, or in a locked session409 ENROLLMENT_LINKED_TO_ROLLOVER / ACADEMIC_SESSION_LOCKEDUse a rollover correction instead of a direct deleteenrollment-removal-guards.ts

11. Diagrams Required Per Module

Covered above: actor capability matrix (§3), high-level flow diagrams (§5), state machines (§7), correction activity diagram (§6.3), data side-effect table (§9).

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
SyncRe-sync is idempotent for terminal itemsAdminPOST /rollover/cycles/:publicId/syncPlaced/graduated/withdrawn pupils unaffectedNo write to terminal itemsrollover-sync.service.ts
SyncAuto-exclusion reason is fixed textSystemNew item with outcome = excludedItem excluded automaticallyexclusionReason = "The current promotion decision outcome is excluded."rollover-sync.service.ts
Section allocationShift-restricted fallbackSystempreserveShift = true, no same-shift class existsStudent becomes eligible for any shift instead of being stuck unassignedshiftRestricted: false returnedsection-allocation.util.ts:117-129
Roll allocationRoll capSystemAny strategyNo roll ever exceeds 999nextFree returns null past 999, item heldroll-allocation.util.ts:18
Roll allocationGrade-scope reservationSystemscope = gradeA roll used in one class of the grade cannot repeat in a sibling classReserved set spans all classes in the graderoll-allocation.util.ts
ProcessingSKIP LOCKEDWorkerConcurrent admin correction on an item mid-runCorrector can lock a row a running processor hasn't reached yet without deadlockingFOR UPDATE SKIP LOCKED on the chunk fetchrollover-runner.service.ts
ProcessingDeferred effects flushed after commitWorkerEvery processed itemCache/audit side effects never fire for a rolled-back itemEffects buffered per item, flushed only post-commitrollover-runner.service.ts
Activationvia: "rollover" bypassSuperadminActivateThe generic in-flight-cycle guard on switchCurrent doesn't self-block the very cycle driving the switchacademic-sessions.service.ts skips its own cycle check for this calleracademic-sessions.service.ts:376-389
CorrectionsAppend-only eventsSystemAny correctionThe event history for a cycle can never be edited or deleted, even by a later correctionrollover_events_append_only trigger raises on UPDATE/DELETE outside a cascade0036_promotions_rollover_guards.sql:131-143
NotificationsNo PII in push/SMSWorkerPlacement notificationGuardian sees a generic "an academic update is available" push, opens the app for detailpromotion-templates.ts withholds student name, marks and reasons from push/SMS bodiespromotion-templates.ts:168-202
SessionsLock blocks cascades except in a cascadeSystemDelete a student in a locked sessionThe lock still permits deleting the pupil themselves (cascade), just not editing their locked-year records directlyTrigger early-returns when pg_trigger_depth() > 10036_promotions_rollover_guards.sql:155-188

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
A cycle cannot be processed with a stale fingerprintPrevents placing pupils against a roster the admin never actually reviewedAdmin must re-review after any changerollover-process-request.service.ts409 ROLLOVER_PREVIEW_STALEFingerprint recomputed and compared before enqueuerollover/shared/__tests__
Processing is superadmin-onlyPlacement is irreversible enough to publish to every family; only the highest trust tier starts itOrdinary admins prepare, superadmin commitsPermission catalog RolloverProcessing403 without the code—permission-catalog.spec.ts
Placement notifications never fire before activationA pupil isn't really "in" the new class until the year is actually switchedGuardians never see a notification for a placement that could still be correctedplacement-notification.service.tsNotification only enqueued at activationNOTIFY_PLACEMENTS conditioned on activated/finalizedrollover/shared/__tests__
A locked session refuses enrollment/class writes at the database levelHistory that has already been activated/finalized cannot be silently rewrittenAny attempt to touch a locked year's records fails, even outside this module0036_promotions_rollover_guards.sql trigger409 ACADEMIC_SESSION_LOCKEDEnforced independent of any service-layer checkacademic-sessions specs
Section capacity is counted, not estimatedPrevents overbooking a physical classroomAdmin sees an accurate SECTION_CAPACITY exception, not a surprise after the factrollover-validation.service.ts:310-335Exception surfaces in /exceptionsused + admitted < capacity, in processOrderrollover-validation.service.ts specs
Manual exclusion always wins over auto-exclusionAn admin's deliberate decision must not be silently reverted by a routine re-syncAdmin can safely re-sync without losing manual workrollover-sync.service.ts—exclusionSource = manual is never overwrittenrollover-sync.service.ts specs

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
Fingerprint-gated processing instead of a simple "confirm" buttonAdmin cannot accidentally process a stale planNo need to re-lock the cycle for reviewA pessimistic lock held from review to processA lock would block all other admin work on the cycle for the review windowA fingerprint miss forces a re-review, which can feel repetitive if changes are frequent
Async chunked processing instead of one synchronous requestLarge schools with thousands of pupils don't time out an HTTP requestPer-item transactions isolate one bad row from the whole runSynchronous bulk transactionA single failing row would either abort everything or require complex partial-commit logicProgress must be polled rather than returned immediately
Superadmin-only gate on the most consequential actions by default, but reconfigurable per schoolSmall schools aren't forced into a rigid hierarchy they don't haveOne documented default instead of per-school special-casingHard-code superadmin-only with no overrideA school could grant these to a role that isn't ready for themDocumented in the permission catalog as a deliberate reversibility tradeoff
Recovery sweep instead of relying on BullMQ's own retryRecovers 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/backoffRequires its own polling cadence and cap (5 recoveries)A genuinely broken run could loop through recoveries before someone notices — bounded by RECOVERY_EXHAUSTED

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
SyncEmpty rosterNo pupil has an active source enrollmentSync returns {inserted: 0, updated: 0}Cycle has zero itemsrollover-sync.service.ts
Section allocation85 into two 40-capacity sectionsbalanced strategy40 assigned to each, 5 left unassignedCount with reason CAPACITYSee Backend §6.4 worked examplesection-allocation.util.ts
Roll allocation60 into three sections, section scopealphabetical strategyEach section independently numbered 1–20See Backend §6.5 worked exampleroll-allocation.util.ts
ProcessingDuplicate placement attemptItem already has a live enrollment when processed againDUPLICATE_PLACEMENT exception, item heldSurfaced under /exceptionsrollover-validation.service.ts
ProcessingConcurrent actionTwo admins correct the same item while a run is in progressSKIP 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 deadlockrollover-runner.service.ts
ProcessingExpired leaseWorker crashes mid-chunkLease expires after 2 minutes, sweep recovers within its 5-minute cadenceAdmin sees runState = running briefly continue via the recovered attemptacademic-year-sweep.service.ts
ActivationPermission mismatchAdmin (not superadmin) calls activate403No state changePermission catalog
CorrectionsMissing dependencyCorrection targets an item with no source enrollment referenceROLLOVER_CORRECTION_INVALIDCorrection rejectedrollover-corrections.service.ts
AllocationUnsupported filterQuery an item list with an unrecognized view valueValidation rejects the request400rollover-item.dto.ts
SessionsQueue failure equivalent — outbox never dispatchedOutbox row written but dispatcher hasn't relayed it yetJob simply hasn't started; the sweep only intervenes once a run is genuinely stalledNo user-visible erroracademic-year-sweep.service.ts

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Processrollover_cycles, rollover_items, rollover_settingsrollover_cycles, rollover_items, student_class_enrollments, student_graduations, rollover_eventsNoneacademic-year queue (PROCESS_ROLLOVER)attempt, itemsToRun
Progressrollover_cycles, rollover_items (counts)NoneNoneNonedisplayStatus, runState, runOutcome, counts

12.7 Experience Quality Checklist

  • 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.

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed.
  • Every actor has allowed and forbidden behavior.
  • Every major and minor flow includes steps, branches, and diagrams.
  • Every lifecycle has a transition table and state diagram.
  • Every flow links to the API and backend docs.
  • Promotion dependency called out where it shapes rollover's behavior.

See Also