Skoolsewa - School Management System Docs
Developer ResourcesAttendance

Attendance Features and Flows

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

Attendance Features and Flows

1. Documentation Evidence

Source TypeFiles or DocsWhat Was Extracted
Backend docapps/fumadocs/content/docs/developer/attendance/backend.mdx, and the services/schema it documentsState model, transaction shapes, side effects, sweep logic.
APIControllers under apps/api/src/modules/attendance/{admin,staff,settings,portal}/Route surface, actors, auth model.
Testsapps/api/src/modules/attendance/__tests__/*.spec.tsConfirmed edge-case behavior for every flow below.
Permissionspackages/db/src/authorization/permission-catalog.ts, packages/db/src/seed/seed-auth.ts, apps/api/src/common/authorization/role.guard.tsWhich actor can reach which flow.

2. Feature Summary

FieldValue
Moduleattendance
SubmoduleStudent register · Staff/teacher register · Notification settings · Teacher/Student/Guardian portal · Reminder and absence sweeps
Primary user valueA class teacher marks a daily register in seconds, an office sees the whole school's attendance state on one screen, a guardian and a pupil are told promptly when the pupil is marked absent, and every correction is preserved as a permanent, tamper-proof history.
ActorsAdministrator/office staff (permissioned), class teacher (audience-asserted, no permission), pupil (audience-asserted), guardian (audience-asserted), the system (two BullMQ sweeps).
Main entry pointsPUT/PATCH/POST /api/attendance/students*, PUT/PATCH /api/attendance/staff*, GET /api/attendance/overview, GET/PUT /api/attendance/settings*, /api/mobile/teacher/attendance*, /api/mobile/student/attendance, /api/mobile/guardian/children/:studentId/attendance, two @Cron(EVERY_MINUTE) sweeps.
Main outputsThe register envelope (session + roster + tally), the school-wide overview, keyset-paginated history pages, push/in-app/SMS/email notifications to a teacher or a guardian.
Related docsAPI: /docs/developer/attendance/api. Backend: /docs/developer/attendance/backend. Dependency: /docs/developer/academic-calendar for the school-day gate this module consumes.

3. Actor Matrix

ActorCan DoCannot DoAuth RequirementNotes
Administrator / office (StudentAttendance_*)Read/mark/correct any class's register; complete/reopen/reset; read the school-wide overview; read any pupil's history.Nothing is withheld from this permission's own scope — it is the unscoped admin surface.JWT + StudentAttendance_READ/CREATE/UPDATE/DELETE.Granted to no seeded role by default — see §12.3. A school creates a dedicated role holding it.
Administrator / office (StaffAttendance_*)Read/mark/correct the staff-and-teacher register on any day, holiday or not; read any staff member's history.—JWT + StaffAttendance_READ/CREATE/UPDATE.Also granted to no seeded role by default.
Administrator (AttendanceSettings_*)Read and update which channels each of the two notification kinds may use.Cannot force a channel a recipient has personally opted out of — the setting can only subtract.JWT + AttendanceSettings_READ/UPDATE.Also granted to no seeded role by default; stays with a dedicated administrator role.
Class teacherList the classes they are the class teacher of; read, mark, and complete THEIR OWN class's register for TODAY only.Mark a date other than today; mark a register once completed; correct a closed day; use force/forcedReason; act on a class they do not teach; reopen or reset a register; see another teacher's classes.JWT; audience-asserted (assertTeacherStaffRow — the active role named teacher, plus a live staff row). No @Permissions().The "holiday override unreachable" rule: the portal DTO carries no force field, and forbidNonWhitelisted turns sending one into a 400. The panel's register screen (/dashboard/my-class/attendance) is gated on the screen-only code AttendanceView_READ, seeded to staff and teacher. The teacher role is granted when a staff member is created on a teaching designation; after a later designation change an administrator grants or revokes it by hand — see PATCH /api/staff/:id in the People API.
PupilRead their own attendance, date-ranged.See another pupil's record; see who marked them; mark or correct anything.JWT; audience-asserted (assertPortalAudience(actor, "student")). No @Permissions().Response carries no marked_by and no staff identity. The panel screen (/dashboard/my-attendance) is gated on the screen-only code AttendanceView_READ, which gates no API route.
GuardianRead ONE named child's attendance, date-ranged — the child must be their ward.See a non-ward's record (404, never 403); see who marked the child.JWT; audience-asserted (assertPortalAudience(actor, "guardian")) plus a ward check. No @Permissions().A non-ward id answers exactly like a non-existent one — the id space is not an enumeration oracle. The panel screens (/dashboard/my-attendance, /dashboard/children/{studentId}/attendance) are gated on AttendanceView_READ, which gates no API route. A superadmin's permission list omits it, so neither screen appears in a superadmin's menu.
System (BullMQ sweeps)Open a register lazily and send a reminder to the class teacher five minutes before first period; alert a pupil's guardians once first period has ended and they are marked absent.Retry the same tick's cohort twice; send when settings are disabled or empty of channels; run outside school hours or on a non-school day.Internal — runs inside the API process, no HTTP surface.Every send is exactly-once via a guarded UPDATE ... WHERE ... IS NULL.

4. Capability Matrix

CapabilitySurfaceActorRoute/TriggerState ReadState WrittenLinked API Section
Read a class's register for a dateAdminAdminGET /api/attendance/studentsCalendar, session, roster—§8.1
Bulk mark/unmark a class's registerAdminAdminPUT /api/attendance/studentsCalendar, session, rosterSession, records, log, runtime§8.2
Correct one pupil's recordAdminAdminPATCH /api/attendance/students/:recordIdRecord, session, subjectRecord, log§8.3
Complete a registerAdmin, TeacherAdmin, TeacherPOST /api/attendance/students/complete, POST /api/mobile/teacher/attendance/completeSession, rosterSession§8.4
Reopen a completed registerAdminAdminPOST /api/attendance/students/reopenSessionSession§8.5
Reset (clear) a registerAdminAdminPOST /api/attendance/students/resetSession, rosterRecords (deleted), log, session, runtime§8.6
Read a pupil's attendance historyAdminAdminGET /api/attendance/students/:studentId/historyChange log—§8.7
Read the school-wide overviewAdminAdminGET /api/attendance/overviewClasses, sessions, records, runtime, timetables, timeslots—§8.8
List the staff/teacher register for a dateAdminAdminGET /api/attendance/staffStaff, records—§8.9
Bulk mark the staff registerAdminAdminPUT /api/attendance/staffStaff, existing recordsRecords, log§8.10
Correct one staff recordAdminAdminPATCH /api/attendance/staff/:recordIdRecordRecord, log§8.11
Read a staff member's historyAdminAdminGET /api/attendance/staff/:staffId/historyChange log—§8.12
Read both notification settingsAdminAdminGET /api/attendance/settingsSettings—§8.13
Update one notification settingAdminAdminPUT /api/attendance/settings/:kindSettingsSettings§8.14
List my classesPortalTeacherGET /api/mobile/teacher/attendance/classesClasses, occupancy—§8.15
Read my class's registerPortalTeacherGET /api/mobile/teacher/attendanceCalendar, session, roster—§8.16
Mark my class's register (today only)PortalTeacherPUT /api/mobile/teacher/attendanceCalendar, session, rosterSession, records, log, runtime§8.17
Complete my class's registerPortalTeacherPOST /api/mobile/teacher/attendance/completeSession, rosterSession§8.18
Read my own attendancePortalPupilGET /api/mobile/student/attendanceRecords—§8.19
Read my child's attendancePortalGuardianGET /api/mobile/guardian/children/:studentId/attendanceWard check, records—§8.20
Read my attendance totalsPortalPupilGET /api/mobile/student/attendance/summaryCurrent session, calendar, enrolments, records (60 s cache)—§8.21
Read my child's attendance totalsPortalGuardianGET /api/mobile/guardian/children/:studentId/attendance/summaryWard check, then as above—§8.22
Send a first-period reminderWorkerSystem@Cron(EVERY_MINUTE) → AttendanceJob.FIRST_PERIOD_REMINDERSettings, calendar, classes, timetables, timeslots, sessions, runtimeSession (lazy open), runtimeBackend §7.7
Send a guardian absence alertWorkerSystem@Cron(EVERY_MINUTE) → AttendanceJob.ABSENCE_ALERTSettings, calendar, school_week_config, recordsguardian_alerted_atBackend §7.8

5. User-Facing Flows

5.1 A class teacher marks today's register

Summary

A class teacher opens their attendance screen, sees the pupils who are on the roll today, marks each present, absent, late or excused, and submits. The system opens the register if this is the first submission of the day, applies the marks, and returns the updated register with a fresh tally.

Preconditions

  • The teacher is authenticated and has a live staff row that is the class_teacher_id of the class they are addressing.
  • The academic calendar has a row for today's date in the class's academic session (otherwise the very first read 409s, prompting the calendar to be generated).
  • The date being marked is today, in the school's own timezone.

Main Flow

StepActor/SystemActionResultSource
1TeacherOpens the attendance screen; the app calls GET /api/mobile/teacher/attendance/classes.The teacher's own classes, with occupancy counts.TeacherAttendancePortalController.listMyClasses
2TeacherSelects a class; the app calls GET /api/mobile/teacher/attendance.The roster with every pupil, marked or not; session: null if never opened today.TeacherAttendancePortalController.getAttendance
3TeacherMarks each pupil, submits PUT /api/mobile/teacher/attendance.The register opens if needed, records are upserted/deleted, a change-log entry is written per real change.TeacherAttendancePortalService.markAttendance → StudentAttendanceWriteService.bulkMark
4BackendRe-reads the full state.Updated roster, tally, session status (advances to in_progress on the first record).StudentAttendanceReadService.loadState
5TeacherScreen shows the confirmed marks and the running tally.——

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Not the class teacherclasses.class_teacher_id !== staffIdRefused before any read/write.403 ATTENDANCE_NOT_CLASS_TEACHER
Date is not todayPortal write with date !== school's todayRefused.422 ATTENDANCE_DATE_NOT_TODAY
Register already completedsession.status === 'completed' and the portal's refuseIfCompleted option is setRefused — correcting a closed day is the admin surface's job.409 ATTENDANCE_ALREADY_COMPLETED
Not a school dayCalendar classification has attendanceExpected: falseRefused; the portal DTO has no force field so a teacher cannot override it.422 ATTENDANCE_NOT_A_SCHOOL_DAY
Repeated submission (double-tap)Same submissionId as the last accepted writeNo-op; returns the current state.200, no new side effects
Concurrent editexpectedVersion staleRefused.409 ATTENDANCE_VERSION_CONFLICT
A pupil in the payload transferred outstudentId has no active enrolment in this classRefused for the whole submission.422 ATTENDANCE_STUDENT_NOT_ENROLLED

5.2 An administrator marks or corrects the register

Summary

An office administrator can do everything the teacher portal can, plus mark any date (past or, with force, a non-school day), correct a completed register per pupil, and run the three lifecycle transitions.

Preconditions

  • The administrator holds StudentAttendance_CREATE/_UPDATE/_DELETE as appropriate — held only by a role the school deliberately created for it.

Main Flow

StepActor/SystemActionResultSource
1AdminGET /api/attendance/students?classPublicId&dateThe register envelope for any date.StudentAttendanceController.getAttendance
2AdminPUT /api/attendance/students with force: true, forcedReason on a non-school day.The register opens and marks are recorded despite the calendar's refusal.StudentAttendanceController.mark
3AdminPATCH /api/attendance/students/:recordId to fix one pupil after the fact.The record and the change log are updated; permitted even on a completed session.StudentAttendanceController.correct
4AdminPOST /api/attendance/students/complete once every pupil is marked.Session becomes completed.StudentAttendanceLifecycleService.complete
5AdminPOST /api/attendance/students/reopen to correct in bulk.Session returns to in_progress.StudentAttendanceLifecycleService.reopen
6AdminPOST /api/attendance/students/reset to clear a mismarked day entirely.Every record for the class/date is deleted, one deletion logged per pupil, session returns to not_started.StudentAttendanceLifecycleService.reset

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Force without a reasonforce: true, forcedReason omittedAccepted — forcedReason is optional even under force; the column is simply left null.200
Non-school day, no forceattendanceExpected: false, force unsetRefused.422 ATTENDANCE_NOT_A_SCHOOL_DAY
No calendar row at allThe calendar has never been generated for this session/dateRefused before the force branch is even reached.409 ACADEMIC_CALENDAR_NOT_GENERATED
Future datedate > todayRefused, admin or teacher.422 ATTENDANCE_DATE_IN_FUTURE
Reset with a stale tokenexpectedVersion mismatchedRefused — the destructive path is guarded exactly like complete and reopen.409 ATTENDANCE_VERSION_CONFLICT
Reset on a register with nothing markedRoster has zero marked pupilsSucceeds as a no-op beyond the session status write — nothing to delete, nothing to log.200
Write against a locked yearThe class's or record's academic session has been lockedRefused before any change reaches the row409 ACADEMIC_SESSION_LOCKED

5.3 A pupil or guardian checks attendance

Summary

A pupil opens their own attendance history; a guardian opens one of their children's. Both see status, class, and note for a date range, with no staff identity attached.

Preconditions

  • The caller holds an active role whose scopeKind matches (student or guardian).
  • For a guardian, the addressed studentId must actually be their ward.

Main Flow

StepActor/SystemActionResultSource
1PupilGET /api/mobile/student/attendance?from&toOwn attendance rows for the range, with Bikram Sambat dates.StudentAttendancePortalController.myAttendance
1'GuardianGET /api/mobile/guardian/children/:studentId/attendance?from&toThe named child's attendance rows, if a ward.GuardianAttendancePortalController.childAttendance

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Not a wardstudentId is not this guardian's child404 — indistinguishable from a non-existent id.404 STUDENT_NOT_FOUND
Guardian's own soft-deleted profileguardians.deleted_at setTreated as not a ward.404 STUDENT_NOT_FOUND
Pupil's own row missingstudents.user_id = actor.id finds nothingRare; the pupil session exists but their profile does not resolve.404 STUDENT_NOT_FOUND
No attendance in the rangeRange genuinely has no recordsEmpty array, not an error.200 []
A date the calendar cannot convert to Bikram SambatOutside BS 2000–2090bsYear/bsMonth/bsDay fall back to 0/0/0 rather than failing the whole page.200, degraded BS fields for that row only

5.3a A pupil or guardian checks the session attendance totals

Summary

A pupil's or guardian's dashboard shows how many days the pupil has been expected, marked, present, late, absent and excused in the current academic session so far, with an attendance rate.

Main Flow

StepActor/SystemActionResultSource
1PupilGET /api/mobile/student/attendance/summaryTotals for the caller's own record; no id is taken from the request.StudentAttendancePortalController.mySummary
1'GuardianGET /api/mobile/guardian/children/:studentId/attendance/summaryTotals for the named child, if a ward.GuardianAttendancePortalController.childSummary
2SystemResolves identity, then reads the totals (cached for 60 seconds per pupil and day).PortalAttendanceSummaryDto.PortalAttendanceSummaryService.forStudent

Branches and Edge Cases

BranchConditionBehaviorError/Result
Not a wardstudentId is not this guardian's child, or the child's graduate access has endedIndistinguishable from a non-existent id.404 STUDENT_NOT_FOUND
Wrong audienceActive role is not a pupil (or guardian) roleRefused before any lookup.403 PERMISSION_INSUFFICIENT
No current sessionNo academic session is flagged currentAll counts 0; session name, bounds and rate null.200
Session not startedToday is before the session startAll counts 0; bounds and rate null, session name set.200
Nothing marked yetmarked is 0ratePct is null; every expected day is unmarked.200
Redis slow or downCache read exceeds its 250 ms budget or failsTotals are read from the database.200

A register mark or correction appears in the totals within 60 seconds.

5.4 An administrator manages the staff/teacher register

Summary

An office administrator marks the staff-and-teacher register for a date — including holidays and exam days, when staff still work — corrects one person, and reads history. There is no completion/reopen/reset cycle for staff; every write simply overwrites.

Preconditions

  • The administrator holds StaffAttendance_READ/CREATE/UPDATE.

Main Flow

StepActor/SystemActionResultSource
1AdminGET /api/attendance/staff?date&designationIdThe full staff roster for the date, paginated, filterable by designation (the Teachers/Staff tabs).StaffAttendanceController.list
2AdminPUT /api/attendance/staffEvery entry upserted or deleted, logged. Last write for the date wins.StaffAttendanceController.bulkWrite
3AdminPATCH /api/attendance/staff/:recordIdOne person's record corrected, keyed on that record's own version.StaffAttendanceController.patch
4AdminGET /api/attendance/staff/:staffId/historyKeyset-paginated change history for that person.StaffAttendanceHistoryController.history

Branches and Edge Cases

BranchConditionBehaviorError/Result
Marked on a holidayCalendar says not a school daySucceeds anyway — the calendar gate is never consulted for staff.200
status: null in the bulk payloadExplicit unmarkDeletes that person's record for the date.200
Duplicate staffId in one payloadSame person twiceRefused for the whole payload.422 ATTENDANCE_DUPLICATE_STUDENT
Two administrators submit the same day at onceNo session/version to detect itThe later commit silently wins; both writes are individually logged.200 for both, no conflict surfaced
Stale PATCHexpectedVersion mismatched on the correction pathRefused — this path DOES have a real row and a real token.409 ATTENDANCE_VERSION_CONFLICT

5.5 The system reminds a teacher and alerts a guardian

Summary

Every minute, two BullMQ jobs tick. Five minutes before a class's first period, if nobody has opened today's register yet, the class teacher gets a reminder. Once first period has ended, any pupil still marked absent triggers a guardian alert — once, ever, per pupil per day.

Preconditions

  • attendance_notification_settings has an enabled row with at least one channel for the relevant kind.
  • The date is a school day with an active academic session covering it.
  • A class must have a published timetable with a first period-type timeslot on today's weekday to be reminded.

Main Flow

StepActor/SystemActionResultSource
1SystemCron ticks every minute; two jobs enqueued with a per-minute jobId.Neither can collapse into the other minute's tick.AttendanceScheduler
2WorkerAttendanceReminderProcessor finds classes in the [start-5, start) window with no reminder sent and a live class teacher.Candidate list.Backend §7.7
3WorkerOpens the register lazily, snapshots the first period, stamps reminder_sent_at guardedly, sends.Teacher gets a push/in-app/SMS/email reminder.NotificationService.send
4WorkerAttendanceAbsenceProcessor scans absent, unalerted records whose session's first period has ended.Candidate list, capped at 500, ordered by id.Backend §7.8
5WorkerStamps guardian_alerted_at guardedly, sends.Guardian gets an alert naming the child.NotificationService.send

Sequence Diagram

Branches and Edge Cases

BranchConditionBehaviorError/Result
Settings row absent, disabled, or channel-lessEither kindSweep logs the skip and sends nothing.No side effect
Tick delayed past the windowA missed or slow tickThe next tick's window still catches the cohort — a persisted marker, not exact-minute equality, is the control.Recovered automatically
Class teacher NULL or soft-deletedclasses.class_teacher_id unset or the staff row is goneThat class is skipped and logged; no crash.No reminder for that class
Register already completedsession.status === 'completed'Skipped — nothing to remind about.No reminder
Register marked during the lesson, not beforeAbsent records exist but the session-level "first period ended" moment already passedStill alerts — the marker is per-record, not per-session, so a late-marked class still triggers the alert once its own record qualifies.Alert sent
Past the school-day cutoffnow.minuteOfDay > school_week_config.school_day_cutoff_minuteAbsence sweep stops entirely for the day.No further alerts that day
More than 500 new absences in one tickLarge school, big rushThe remaining rows are picked up by the next tick, in deterministic id order.Delayed by up to a minute, never lost

6. Admin Flows

6.1 Create (mark)

PUT /api/attendance/students and PUT /api/attendance/staff both create records where none existed. Neither has a separate "create" verb from "update" — a register is opened lazily on the first mark.

6.2 List / Read detail

GET /api/attendance/students, GET /api/attendance/staff, GET /api/attendance/overview — see §8 in the API doc for full parameters.

6.3 Update (correct)

PATCH /api/attendance/students/:recordId and PATCH /api/attendance/staff/:recordId — both keyed on the record's own version, both permitted regardless of session/day state.

6.4 Delete

There is no per-record delete route. Deletion happens two ways: an entry with status: null in a bulk payload deletes that person's record; POST /api/attendance/students/reset deletes every record for a class/date at once.

6.5 Reorder

Not applicable — attendance has no orderable list.

6.6 Activate/deactivate

Not applicable to records. The closest analogue is attendance_notification_settings.enabled, covered in §6.9 below.

6.7 Soft delete / Restore

Not applicable to attendance records themselves — see §6.4 for the actual deletion semantics. Soft-deleted students/staff rows are read from, not written to, by this module (see backend doc §6.3).

6.8 Export/import

Not present in this module.

6.9 Notification settings — the module's own "configure" flow

AttendanceSettings_UPDATE holders can only ever narrow delivery — the per-recipient preference resolver runs afterwards and always wins in the restrictive direction.

6.10 Manual retry

There is no manual retry endpoint. A missed sweep tick recovers automatically on the next tick because both sweeps select over a window with a persisted marker rather than exact-minute equality.

7. Lifecycle and State Transitions

7.1 Student register session lifecycle

EntityFromEvent/ActionToGuard ConditionSide Effects
student_attendance_sessions(none)First PUT marknot_startedCalendar allows it (or force)Session row created
student_attendance_sessionsnot_startedFirst record actually writtenin_progressAt least one record existsConditional UPDATE
student_attendance_sessionsin_progressPOST .../completecompletedEvery live roster row has a recordcompleted_at, completed_by_user_id set
student_attendance_sessionscompletedPOST .../reopenin_progressexpectedVersion matchescompleted_at, completed_by_user_id cleared
student_attendance_sessionsanyPOST .../resetnot_startedexpectedVersion matchesEvery record deleted, one deletion logged per pupil, runtime's last_submission_id cleared

7.2 Per-record status

A student_attendance_records/staff_attendance_records row has no internal lifecycle of its own — its status is one of present/absent/late/excused, and "unmarked" is the row's absence rather than a fifth value.

9. Data and Side Effects by Flow

FlowDB WritesCache EffectsJobsRealtimeAnalyticsNotifications
Bulk mark (admin or teacher)student_attendance_sessions, student_attendance_records, attendance_change_log, student_attendance_session_runtimeNoneNoneNoneNoneNone
Correct one recordstudent_attendance_records, attendance_change_logNoneNoneNoneNoneNone
Complete/reopenstudent_attendance_sessionsNoneNoneNoneNoneNone
Resetstudent_attendance_records (deleted), attendance_change_log, student_attendance_sessions, student_attendance_session_runtimeNoneNoneNoneNoneNone
Staff bulk write / patchstaff_attendance_records, attendance_change_logNoneNoneNoneNoneNone
Update a settingattendance_notification_settingsNoneNoneNoneNoneNone
Reminder sweep tickstudent_attendance_sessions (lazy open), student_attendance_session_runtimeNoneConsumes AttendanceJob.FIRST_PERIOD_REMINDERNoneNonePush/in-app/SMS/email to the class teacher via NotificationService.send
Absence sweep tickstudent_attendance_records.guardian_alerted_atNoneConsumes AttendanceJob.ABSENCE_ALERTNoneNonePush/in-app/SMS/email to the pupil's guardians

10. Error and Recovery Flows

ScenarioTriggerUser/System ExperienceRecoverySource
Calendar not generatedSession/date has no academic_calendar_days rowEvery read and write on that date 409sGenerate the calendar for the session firstCalendarDayService.require
Concurrent editTwo operators read the same register, both submitThe second submit 409sReload and resubmit against the current versionATTENDANCE_VERSION_CONFLICT
Double-tap / network retryClient resends the exact same submissionNo error, no duplicate effectNothing to do — it is already appliedsubmissionId idempotency
Missed cron tickScheduler enqueue fails, or a worker tick is delayedNo visible error to any userThe next tick's window recovers the cohortServer log only — see backend doc §14
Settings row missingMigration seed not appliedGET /attendance/settings 404s; both sweeps silently send nothingRestore the seed rows from migration 0020ATTENDANCE_SETTINGS_NOT_FOUND
Malformed pagination cursorA hand-built or corrupted cursor query paramHistory page 400sRestart from page one (omit cursor)PAGINATION_CURSOR_INVALID

11. Diagrams Required Per Module

  • Actor capability diagram: §3, §4.
  • High-level module flow diagram: §5.5's sequence diagram covers the fully automated path; §5.1/§5.2 cover the human-triggered ones.
  • Sequence diagram for each major flow: §5.1–§5.5.
  • State machine diagram for every lifecycle: §7.1, §7.2.
  • Data side-effect diagram for write flows: §9, and §12.6 below.
  • Error branch diagram for critical flows: §10, and the backend doc's error-branch table (§13 there).

12. Mandatory Feature and Flow Deep-Dive Pack

12.1 Feature Inventory With Minor Behaviors

FeatureMinor BehaviorActorTriggerUser/System ResultBackend Side EffectSource
Bulk markOmitted pupil is left unchanged, never implicitly unmarkedAdmin/teacherPUT with entries not covering every pupilOnly the submitted pupils changePartial upsert/delete, not a full-roster rewritestudent-attendance-write.service.ts:260-268
Bulk markstatus: null in an entry is an explicit unmarkAdmin/teacherEntry with status: nullThat pupil's record is deletedDELETE ... studentId IN (...)dto/student-attendance.dto.ts:53-57
Bulk markIdempotent replayAdmin/teacherSame submissionId resentNo visible changeShort-circuits to a re-read before the concurrency checkstudent-attendance-write.service.ts:202-212
Bulk markRoll order, unrolled pupil lastAdmin/teacher/system (overview)Any roster readPupils appear in roll order, no-roll lastORDER BY roll_number ASC NULLS LAST, admission_number, idattendance-roster.service.ts:93-101
Bulk markForce-marking a non-school day requires a stated reason column, kept separate from the noteAdmin onlyforce: trueforced_reason populated, distinct from any operator noteSeparate columns, separate length CHECKsattendance.ts:234-241
CompleteRefuses on ANY unmarked live pupil, including one added mid-dayAdmin/teacherPOST .../complete422 until every current roll member has a recordEvaluated against a live roster read, never a stored countstudent-attendance-lifecycle.service.ts:79-89
ResetLogs the deletion of EVERY previously-marked pupil, one row eachAdminPOST .../resetHistory shows the reset as a series of newStatus: null entriesAttendanceLogService.append given the pre-image rosterstudent-attendance-lifecycle.service.ts:182-198
HistoryThe first marking is a history entry tooAdminAny first-ever mark of a pupilHistory is never empty for a marked pupilpreviousStatus: null rows are written, not filtered outattendance-log.service.ts:13-19
HistoryKeyset pagination survives a same-timestamp tieAdminBulk mark of many pupilsNo row dropped or duplicated across a page boundary(changed_at, id) cursor, truncated/rendered for precisionattendance-history.service.ts:29-41
OverviewSchedule state distinguishes "no timetable" from "no period today"AdminGET /attendance/overviewTwo different, informative states rather than one generic "not scheduled"Two separate lookups (class_timetables.status, class_timeslots by weekday)attendance-overview.service.ts:94-98
OverviewUnalerted-absence count is exposed per classAdminGET /attendance/overviewThe office can see, before any guardian complains, which classes have pending alertscount(*) FILTER (WHERE status='absent' AND guardian_alerted_at IS NULL)attendance-overview.service.ts:170
Staff registerHoliday marking succeeds where student marking would refuseAdminPUT /attendance/staff on a non-school day200, recorded normallyThe calendar's attendanceExpected half is never called on this pathstaff-attendance.service.ts:180-182
Staff registerDuplicate payload entry refused for the whole submissionAdminSame staffId twice in one PUT422, nothing writtenChecked before the transaction opensstaff-attendance.service.ts:194-204
SettingsAn update can only restrict delivery, never widen itAdminPUT /attendance/settings/:kindEnabling email/sms here changes nothing for a recipient who never opted inCATEGORY_DEFAULTS.attendance applies afterwards, unconditionallydto/attendance-settings.dto.ts:14-31
Teacher portalforce/forcedReason are unreachable, not merely unusedTeacherAny PUT payload including those fields400 validation errorDTO omits the fields; forbidNonWhitelisted rejects extrasdto/teacher-attendance-portal.dto.ts:30-53
Guardian/Student portalNo staff identity in the responsePupil/GuardianAny readResponse has no markedBy/staff name field at allDTO enumerates fields by hand, never spreads the wider entitydto/student-attendance-portal.dto.ts:16-38
SweepsA class with no class teacher, or a soft-deleted one, is silently skipped, not erroring the whole tickSystemReminder sweepEvery other eligible class is still processedPer-candidate try, logged warn, loop continuesattendance-reminder.processor.ts:314-331
SweepsAbsence sweep batches at 500, ordered by idSystemA morning with unusually many absencesRemaining rows picked up next tick, none lost or duplicatedLIMIT 500 ORDER BY idattendance-absence.processor.ts:30,229-230

12.2 Business Process Diagram Pack

12.3 Business Rules and Policy Traceability

RuleBusiness ReasonActor ImpactEnforced InAPI ImpactBackend ImpactTests
Unmarked is a missing row, never a stored statusKeeps the enum closed over real outcomes; a placeholder status could answer "does this exist" wronglyA pupil with no record shows a neutral "not yet marked" state, distinct from any real statusSchema (status NOT NULL), service (status: null deletes)MarkAttendanceEntryDto.status is nullable and means deleteattendance.ts enum commentstudent-attendance.integration.spec.ts
Calendar gate refuses a non-school-day mark unless forcedThe academic calendar is the single source of truth for whether the school operates on a dateA teacher cannot accidentally mark a holiday; an administrator can, with a stated reasonStudentAttendanceWriteService.bulkMarkforce/forcedReason on the admin DTO onlyCalendarDayService.require"refuses a holiday without force, and succeeds with force"
Completion requires every live roster row markedA tally derived from an incomplete roster would misrepresent the dayTeachers must account for every pupil before closing the dayStudentAttendanceLifecycleService.completePOST .../complete 422s otherwiseEvaluated against a live read, not a counter"refuses to complete an incomplete roster..."
Teacher portal is today-only, not-completed-onlyCorrecting history is an administrative act, behind its own permissionA teacher cannot silently rewrite a closed day through an un-permissioned routebulkMark's requireToday/refuseIfCompleted optionsPortal PUT 422/409s outside today or once completedSame transaction, different options"refuses a non-today date on the teacher's write"
Staff register has no calendar gateStaff legitimately work on holidays and exam days; the register must be able to describe those daysAdmin can mark staff attendance any day without a workaroundStaffAttendanceService.bulkWrite never calls the gate's expectation halfNo force field needed or present—"marks staff attendance on a holiday successfully"
No seeded role holds any attendance permission codeTEACHER_PERMISSIONS spreads STAFF_PERMISSIONS; both carry scopeKind: "all", so a grant to staff would give every teacher the unscoped admin surfaceTeachers reach attendance only through the audience-asserted portal, never the admin routesseed-auth.ts (absence of grants)Admin routes 403 for a teacher session without a dedicated rolepermission-catalog.ts commentPermission catalogue and seed review
A guardian's read never filters record_statusThat column is an administrator's record-keeping toggle, not an existence signal; filtering it here would make a parent's read stricter than the pupil's ownA parent keeps seeing their child's attendance even if an unrelated record-keeping flag changesGuardianAttendancePortalService.resolveWard404 only on a genuine non-ward, never on this toggleIdentity layer filters deleted_at onlyRoot CLAUDE.md's identity-vs-row-scope rule
Notification settings can only subtractattendance is not an unsuppressible category; an administrator must not override a guardian's own opt-out at metered costA guardian's personal preference always wins over the school's channel settingAttendanceSettingsService + the notification preference resolverThe settings screen must say this in wordsCATEGORY_DEFAULTS.attendance applied unconditionally afterwardsattendance-settings.integration.spec.ts
Sweeps select over a window with a persisted markerExact-minute equality would drop a whole minute's cohort permanently on any delayNobody's reminder or alert is silently lost to a slow tickBoth processors' findCandidatesNo client-visible contract — purely operationalGuarded UPDATE ... WHERE ... IS NULLattendance-sweeps.integration.spec.ts
A locked academic year's register, staff record and student record can no longer changeThe year's academic record is closed once locked; only an administrator reopening it can reintroduce a writeEvery write route answers a single, named refusal instead of a constraint violationPostgres trigger on the three tables (see the academic-sessions backend doc)409 ACADEMIC_SESSION_LOCKED on any affected routeassert_academic_session_unlocked / _via—

12.4 Tradeoffs and Product Rationale

Product DecisionUser BenefitEngineering BenefitAlternativeTradeoffRisk
The register is a set of changes, not a full-state rewriteA teacher can mark ten of thirty pupils and come back later without re-sending everyoneSmaller payloads, and a partial submission cannot accidentally wipe unrelated marksRequire the full roster in every PUTA client must track which pupils it has already sent, across screensLow — the read endpoint always returns the current full state to reconcile against
Idempotency via submissionId, not by making the write itself idempotentA flaky connection's retry never double-applies or conflicts against itselfThe concurrency check stays a simple, honest optimistic-lock comparisonRely on the upsert's natural idempotency and accept a version bump on replayOne extra column, one extra readNone material
Staff attendance is last-write-winsThe common case — one administrator, one register — never has to think about a version tokenNo synthetic session row, no extra join, for a surface with genuinely low write contentionGive staff the same session+version machinery as studentsA genuine simultaneous edit from two admins silently loses the earlier writeLow — this is a single-office, low-frequency surface
Force-marking is admin-onlyA teacher cannot accidentally create attendance records for a day the school did not operateKeeps the calendar's refusal meaningful on the surface most people useLet any marker forceA holiday that genuinely needs recording (a special event day) requires an administrator, not the class teacher, to record itLow — matches this module's actual authority model
No stored attendance counters anywhereThe numbers on screen can never contradict the list beneath themRemoves an entire class of drift bug this module already shipped once and fixedMaintain running counters, updated per writeAn extra count(*) FILTER per readNone at classroom/school scale

12.5 Flow Edge-Case Matrix

FlowEdge CaseTriggerExpected BehaviorUser/System FeedbackSource
Bulk markEmpty stateentries: []Refused before any read422 ATTENDANCE_EMPTY_PAYLOADstudent-attendance-write.service.ts:92-97
Bulk markFirst useNo session row exists yet for this class/dateSession opened lazily on the first mark200, session was null, now populatedstudent-attendance.integration.spec.ts
Bulk markLast itemThe final unmarked pupil on the roll is markedSession becomes eligible for completeNo error; complete now succeedsstudent-attendance-lifecycle.service.ts:84-89
Bulk markDuplicate actionSame submissionId twiceNo-op200, unchanged state"replaying the same submissionId writes nothing and returns 200"
Bulk markConcurrent actionTwo operators, stale expectedVersionSecond submit refused409 ATTENDANCE_VERSION_CONFLICT"refuses a stale expectedVersion as a conflict"
Bulk markExpired stateN/A — there is no session expiry in this module———
Teacher portalPermission mismatchA staff session that is not a class teacher of the addressed classRefused403 ATTENDANCE_NOT_CLASS_TEACHER"refuses a teacher who is not the class teacher"
Guardian portalGuest limitationN/A — no guest access exists on this module; every route requires an authenticated, role-scoped session———
Guardian portalMissing dependencyThe child is not (or no longer) this guardian's ward404404 STUDENT_NOT_FOUND"refuses a child who is not this guardian's ward"
HistoryCache stale/missN/A — this module has no cache layer———
HistoryUnsupported filter or sortN/A — history takes only from/to/cursor/size, all validated———
Reminder sweepQueue failureattendanceQueue.add throwsLogged, cron continues; the tick is simply skippedServer log onlyattendance.scheduler.ts:51-58
Absence sweepCache stale/missN/A———

12.6 Flow-to-Data Trace

FlowReadsWritesCacheJobs/EventsResponse Fields
Bulk markclasses, sessions, academic sessions, calendar, roster, runtimesessions, records, log, runtimeNoneNoneclassLabel, calendar, session, version, roster[], tally
Complete/reopen/resetsessions, rostersessions (+ records/log/runtime on reset)NoneNoneSame envelope as bulk mark
Overviewclasses, sessions, records, runtime, timetables, timeslotsNoneNoneNoneOne row per class: tallies, scheduleState, reminderSentAt, unalertedAbsences
History (either)change logNoneNoneNonedata[] array, top-level nextCursor
Reminder sweepsettings, calendar, classes, timetables, timeslots, sessions, runtime, staffsessions (lazy open), runtimeNoneConsumes one job per tick(no HTTP response — a notification)
Absence sweepsettings, school_week_config, calendar, records joined through session/runtime/class/student/userguardian_alerted_atNoneConsumes one job per tick(no HTTP response — a notification)

12.7 Experience Quality Checklist

  • The doc explains what the actor is trying to accomplish (§5).
  • The doc explains what the backend does that the actor does not see (§12.1, §12.6).
  • The doc covers every minor flow and branch (§12.1, §12.5).
  • The doc includes user, admin, worker, and system flows (§5, §6, §5.5).
  • The doc explains business logic, tradeoffs, and rationale (§12.3, §12.4).
  • The doc maps every flow to API routes and backend side effects (§4, §9, §12.6).
  • The doc includes diagrams appropriate to each flow type (§5, §7, §12.2, §12.6).
  • The doc covers all edge cases and failure recovery (§10, §12.5).

13. Completion Checklist

  • Every feature, minor action, and submodule capability is listed (§4, §12.1).
  • Every actor has allowed and forbidden behavior (§3).
  • Every major and minor flow includes steps, branches, and diagrams (§5, §6).
  • Every lifecycle has a transition table and state diagram (§7).
  • Every flow links to the API and backend docs (below, and inline throughout).
  • Dependencies on other modules are called out — the academic calendar (see below) shapes when attendance may be marked, and this doc documents only that dependency, not the calendar module itself.

See Also

  • API doc: /docs/developer/attendance/api
  • Backend doc: /docs/developer/attendance/backend
  • Academic calendar (dependency — not documented here): /docs/developer/academic-calendar

On this page

Attendance Features and Flows1. Documentation Evidence2. Feature Summary3. Actor Matrix4. Capability Matrix5. User-Facing Flows5.1 A class teacher marks today's registerSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.2 An administrator marks or corrects the registerSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.3 A pupil or guardian checks attendanceSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases5.3a A pupil or guardian checks the session attendance totalsSummaryMain FlowBranches and Edge Cases5.4 An administrator manages the staff/teacher registerSummaryPreconditionsMain FlowBranches and Edge Cases5.5 The system reminds a teacher and alerts a guardianSummaryPreconditionsMain FlowSequence DiagramBranches and Edge Cases6. Admin Flows6.1 Create (mark)6.2 List / Read detail6.3 Update (correct)6.4 Delete6.5 Reorder6.6 Activate/deactivate6.7 Soft delete / Restore6.8 Export/import6.9 Notification settings — the module's own "configure" flow6.10 Manual retry7. Lifecycle and State Transitions7.1 Student register session lifecycle7.2 Per-record status9. Data and Side Effects by Flow10. Error and Recovery Flows11. Diagrams Required Per Module12. Mandatory Feature and Flow Deep-Dive Pack12.1 Feature Inventory With Minor Behaviors12.2 Business Process Diagram Pack12.3 Business Rules and Policy Traceability12.4 Tradeoffs and Product Rationale12.5 Flow Edge-Case Matrix12.6 Flow-to-Data Trace12.7 Experience Quality Checklist13. Completion ChecklistSee Also