Attendance API Reference
Complete API contracts for Attendance, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Attendance - API Reference
Audience: Frontend engineers, mobile engineers, backend engineers, QA, and API consumers. Scope: Admin (student and staff registers, overview, history, settings) and mobile/consumer (teacher, student, guardian portal) APIs owned by Attendance.
1. Documentation Evidence
| Area | Files Inspected | What Was Verified |
|---|---|---|
| Controllers | apps/api/src/modules/attendance/admin/*.controller.ts, staff/*.controller.ts, settings/attendance-settings.controller.ts, portal/{teacher,student,guardian}/*.controller.ts | Routes, methods, guards, decorators, status codes, Swagger tags. |
| DTOs | apps/api/src/modules/attendance/admin/dto/*.ts, staff/dto/*.ts, settings/dto/*.ts, portal/{teacher,student,guardian}/dto/*.ts | Request, query, response, validation, examples. |
| Services | apps/api/src/modules/attendance/{admin,staff,settings,portal,shared}/*.service.ts | Behavior, side effects, response mapping, errors. |
| Schema | packages/db/src/schema/school/attendance.ts, packages/db/src/migrations/0020_attendance_module.sql | IDs, enums, persisted fields, constraints. |
| Queue | packages/jobs/src/index.ts (QueueName.ATTENDANCE, AttendanceJob, AttendanceSweepPayload) | Async job contract behind the two sweeps. |
| Error registry | apps/api/src/common/types/error-codes.ts | Every ATTENDANCE_*/STAFF_ATTENDANCE_* code. |
| Permissions/guard | packages/db/src/authorization/permission-catalog.ts, packages/db/src/seed/seed-auth.ts, apps/api/src/common/authorization/role.guard.ts, apps/api/test/structure/route-permissions.spec.ts | Permission codes, seeded grants (none), and the no-permission allowlist for the six portal handlers. |
| Tests | apps/api/src/modules/attendance/__tests__/*.spec.ts | Confirmed edge cases and example behaviors used below. |
2. Module Summary
| Field | Value |
|---|---|
| Module name | Attendance |
| Module slug | attendance |
| Primary actors | Administrator (StudentAttendance_*, StaffAttendance_*, AttendanceSettings_*), class teacher (audience staff, no permission), pupil (audience student, no permission), guardian (audience guardian, no permission), internal system (BullMQ) |
| API surfaces | admin (registered in ADMIN_MODULES via AttendanceAdminModule, AttendanceStaffModule, AttendanceSettingsModule), mobile (registered in MOBILE_CHILDREN via TeacherAttendancePortalModule, StudentAttendancePortalModule, GuardianAttendancePortalModule) |
| Base route prefixes | /api/attendance/students, /api/attendance/staff, /api/attendance/overview, /api/attendance/settings, /api/mobile/teacher/attendance, /api/mobile/student/attendance, /api/mobile/guardian/children |
| Auth model | JwtAuthGuard (global) + RoleGuard on every route. Admin/staff/settings routes additionally carry @Permissions(). All six portal routes carry no @Permissions() and are audience-asserted instead (assertPortalAudience / assertTeacherStaffRow). |
| Persistence | PostgreSQL only — no Redis cache, no MongoDB collection owned by this module. |
| Runtime source of truth | student_attendance_sessions/_records, staff_attendance_records, attendance_change_log, attendance_notification_settings; the academic calendar and academic sessions are read-only dependencies owned elsewhere. |
| Sibling docs | Backend: /docs/developer/attendance/backend. Features/flows: /docs/developer/attendance/feature. |
3. Concepts and Terminology
| Term | Meaning | Source File | Used By |
|---|---|---|---|
| Register / session | One class's attendance record for one date — student_attendance_sessions | attendance.ts:90 | Every student-attendance endpoint |
| Runtime row | The system-written scheduling facts for a session, carrying no version | attendance.ts:161 | The reminder sweep, the idempotency check |
| Record | One pupil's or staff member's status for one day | attendance.ts:210,329 | Every mark/correct endpoint |
| Change log | The append-only history of every attendance transition, including the first marking | attendance.ts:405 | Both history endpoints |
| Status | present | absent | late | excused — closed, no "unmarked" member | attendance.ts:60-65 | Every DTO carrying a status |
| Session status | not_started | in_progress | completed — the register's own lifecycle | attendance.ts:75-79 | StudentAttendanceSessionDto |
| Subject kind | student | staff — which population a change-log row describes | attendance.ts:81-84 | AttendanceHistoryEntryDto/staff equivalent |
| Force write | An administrator's override of the calendar's "not a school day" refusal, with a stated reason | dto/student-attendance.dto.ts:103-114 | BulkMarkAttendanceDto (admin only) |
| Expected version | The optimistic-concurrency token — the session's or the record's own version | schema version columns | Every mutating request except the staff bulk write |
| Submission id | A client-generated id that makes a bulk write idempotent | attendance.ts:188-196 | BulkMarkAttendanceDto, TeacherMarkAttendanceDto |
| Attendance expected | Whether the calendar classification requires attendance on a date, derived, never stored | calendar-contract.ts:115-121 | AttendanceCalendarDayDto.attendanceExpected |
| Roster | Every live pupil on a class's roll for a date, with their record left-joined on | attendance-roster.service.ts:126-142 | StudentAttendanceStateDto.roster |
| Tally | Counts derived from one roster read, never stored | attendance-roster.service.ts:144-152 | AttendanceTallyDto, StaffAttendanceTallyDto |
| Schedule state | published | no_published_timetable | no_period_today — a class's timetable readiness for a date | dto/attendance-overview.dto.ts:12-18 | AttendanceOverviewRowDto |
4. API Surface Map
| Surface | Method | Path | Actor | Auth/Guard | Permission | Controller | Purpose |
|---|---|---|---|---|---|---|---|
| Admin | GET | /api/attendance/students | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_READ | StudentAttendanceController | Read a class's register for one date. |
| Admin | PUT | /api/attendance/students | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_CREATE, StudentAttendance_UPDATE | StudentAttendanceController | Bulk mark or correct a class's register. |
| Admin | PATCH | /api/attendance/students/:recordId | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_UPDATE | StudentAttendanceController | Correct one pupil's record. |
| Admin | POST | /api/attendance/students/complete | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_UPDATE | StudentAttendanceController | Complete a register. |
| Admin | POST | /api/attendance/students/reopen | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_UPDATE | StudentAttendanceController | Reopen a completed register. |
| Admin | POST | /api/attendance/students/reset | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_DELETE | StudentAttendanceController | Clear a register entirely. |
| Admin | GET | /api/attendance/students/:studentId/history | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_READ | StudentAttendanceHistoryController | A pupil's attendance change log. |
| Admin | GET | /api/attendance/overview | Admin | JwtAuthGuard, RoleGuard | StudentAttendance_READ | AttendanceOverviewController | The whole school's attendance state for one date. |
| Admin | GET | /api/attendance/staff | Admin | JwtAuthGuard, RoleGuard | StaffAttendance_READ | StaffAttendanceController | List the staff register for one date. |
| Admin | PUT | /api/attendance/staff | Admin | JwtAuthGuard, RoleGuard | StaffAttendance_CREATE, StaffAttendance_UPDATE | StaffAttendanceController | Mark the staff register for one date. |
| Admin | PATCH | /api/attendance/staff/:recordId | Admin | JwtAuthGuard, RoleGuard | StaffAttendance_UPDATE | StaffAttendanceController | Correct one staff attendance record. |
| Admin | GET | /api/attendance/staff/:staffId/history | Admin | JwtAuthGuard, RoleGuard | StaffAttendance_READ | StaffAttendanceHistoryController | One staff member's attendance change history. |
| Admin | GET | /api/attendance/settings | Admin | JwtAuthGuard, RoleGuard | AttendanceSettings_READ | AttendanceSettingsController | Read both attendance notification settings. |
| Admin | PUT | /api/attendance/settings/:kind | Admin | JwtAuthGuard, RoleGuard | AttendanceSettings_UPDATE | AttendanceSettingsController | Update one attendance notification setting. |
| Mobile | GET | /api/mobile/teacher/attendance/classes | Teacher | JwtAuthGuard, RoleGuard | None — audience-asserted | TeacherAttendancePortalController | The classes I am the class teacher of. |
| Mobile | GET | /api/mobile/teacher/attendance | Teacher | JwtAuthGuard, RoleGuard | None — audience-asserted | TeacherAttendancePortalController | Read one of my classes' register for one date. |
| Mobile | PUT | /api/mobile/teacher/attendance | Teacher | JwtAuthGuard, RoleGuard | None — audience-asserted | TeacherAttendancePortalController | Mark today's register for one of my classes. |
| Mobile | POST | /api/mobile/teacher/attendance/complete | Teacher | JwtAuthGuard, RoleGuard | None — audience-asserted | TeacherAttendancePortalController | Complete one of my classes' register. |
| Mobile | GET | /api/mobile/student/attendance | Pupil | JwtAuthGuard, RoleGuard | None — audience-asserted | StudentAttendancePortalController | My own attendance, date-ranged. |
| Mobile | GET | /api/mobile/student/attendance/summary | Pupil | JwtAuthGuard, RoleGuard | None — audience-asserted | StudentAttendancePortalController | My attendance totals for the current academic session. |
| Mobile | GET | /api/mobile/guardian/children/:studentId/attendance | Guardian | JwtAuthGuard, RoleGuard | None — audience-asserted | GuardianAttendancePortalController | One of my children's attendance, date-ranged. |
| Mobile | GET | /api/mobile/guardian/children/:studentId/attendance/summary | Guardian | JwtAuthGuard, RoleGuard | None — audience-asserted | GuardianAttendancePortalController | One of my children's attendance totals for the current academic session. |
AttendanceSharedModule and AttendanceWorkersModule declare no controller and contribute nothing to either Swagger document — the two BullMQ sweeps have no HTTP surface at all.
5. Auth, Identity, and Permissions
| Surface | Guard/Decorator | Identity Shape | Permission | Guest Allowed | Notes |
|---|---|---|---|---|---|
| Admin (student, staff, settings) | JwtAuthGuard, RoleGuard, @Permissions(...) | req.user, activeRole | As listed in §4 | No | None of these three permission modules is granted to any seeded role — see below. |
| Teacher portal | JwtAuthGuard, RoleGuard, no @Permissions() | req.user acting as the role named teacher, resolved to a live staff.id via assertTeacherStaffRow | N/A | No | Handler key must be in NO_PERMISSION_ADMIN_HANDLERS (role.guard.ts) and its mirror route-permissions.spec.ts, or every caller including superadmin gets 403. |
| Student portal | JwtAuthGuard, RoleGuard, no @Permissions() | req.user, matched to students.user_id | N/A | No | Takes no id parameter at all — the pupil is resolved entirely from the token. |
| Guardian portal | JwtAuthGuard, RoleGuard, no @Permissions() | req.user, matched through guardians.user_id then a ward check | N/A | No | A non-ward studentId answers 404, never 403. |
Why no seeded role holds StudentAttendance_*, StaffAttendance_* or AttendanceSettings_*. packages/db/src/seed/seed-auth.ts grants none of these codes to any role. TEACHER_PERMISSIONS spreads STAFF_PERMISSIONS in that file, so anything granted to staff reaches every teacher, and both seeded roles carry scopeKind: "all" — so granting StudentAttendance_READ to staff would give every teacher an unscoped read over every pupil's attendance and history, with none of the class-teacher predicate the portal enforces. A school that wants a non-administrator to hold these codes creates a dedicated role for it; grantToRoles never revokes a grant, so withholding by default is the recoverable direction.
AttendanceView — the screen code, which gates no route. A fourth module, AttendanceView, gates the panel's attendance screens: a pupil's own (/dashboard/my-attendance), a guardian's per-child view (/dashboard/children/{studentId}/attendance) and a class teacher's register (/dashboard/my-class/attendance), plus their sidebar entries. It appears on no API route — the three portal surfaces above carry no @Permissions() and assert their audience instead — so holding it grants no data access. AttendanceView_READ is seeded to staff (and through the spread to teacher), guardian and student. It is one of the person-bound screen modules a superadmin's GET /api/auth/permissions list omits, because a superadmin account has no pupil, ward or class of its own; see the auth API page, §3.5. The calendar code, AcademicCalendarView_READ, stays separate because the calendar is also an administrator screen.
The portal's audience assertion is not a permission, and is not a row scope either. assertPortalAudience(actor, "student" | "guardian") and assertTeacherStaffRow(db, actor) (apps/api/src/modules/mobile/shared/portal-audience.ts:74,176) prove only who is calling. Which rows come back is decided entirely by the query each service builds — classes.class_teacher_id = staffId for the teacher, students.user_id = actor.id for the pupil, the student_guardian join for the guardian. None of the three ever delegates to PeopleAccessService.scopeFor, whose {kind: "all"} result for a staff/teacher session would defeat the whole point of a per-route predicate.
6. DTO and Model Reference
6.1 MarkAttendanceEntryDto (nested request)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
studentId | string (uuid) | Yes | N/A | @IsUUID | "018f..." | dto/student-attendance.dto.ts:44-47 |
status | "present"|"absent"|"late"|"excused"|null | Yes (present in payload) | N/A | @IsIn(ATTENDANCE_STATUS), nullable | "present" | :54-57 |
note | string | null | No | N/A | @MaxLength(500), no leading/trailing whitespace, no control characters | "Called in sick" | :59-64 |
status: null deletes that pupil's record for the date rather than storing a null status.
6.2 BulkMarkAttendanceDto (request, admin PUT)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
classPublicId | string (uuid) | Yes | N/A | @IsUUID | — | :73-75 |
date | string | Yes | N/A | @IsDateString | "2026-09-13" | :77-79 |
expectedVersion | number | null | Yes (present in payload) | N/A | @IsInt, @Min(0), nullable | 3 | :86-90 |
submissionId | string (uuid) | Yes | N/A | @IsUUID | — | :92-95 |
entries | MarkAttendanceEntryDto[] | Yes | N/A | @ArrayMinSize(1), @ValidateNested | — | :97-101 |
force | boolean | No | N/A | @IsBoolean | true | :103-107 |
forcedReason | string | No | N/A | @MaxLength(200), whitespace/control-char format | "Emergency makeup class" | :109-114 |
expectedVersion: null means "I read a class/date with no session row yet" — the first write of the day has no token to send.
6.3 GetStudentAttendanceQueryDto (query, GET .../students)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
date | string | Yes | N/A | @IsDateString | "2026-09-13" | :118-120 |
classPublicId | string (uuid) | Yes | N/A | @IsUUID | — | :122-124 |
6.4 PatchStudentAttendanceRecordDto (request, PATCH .../:recordId)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
status | "present"|"absent"|"late"|"excused" | Yes | N/A | @IsIn(ATTENDANCE_STATUS) (not nullable — PATCH always addresses an existing record) | "present" | :131-133 |
note | string | null | No | N/A | Same format rule as MarkAttendanceEntryDto.note | — | :135-140 |
reason | string | No | N/A | @MaxLength(500), same format rule; written to the CHANGE LOG's reason, not to the record | "Parent called in" | :142-148 |
expectedVersion | number | Yes | N/A | @IsInt, @Min(0) (never null — a record to PATCH already has a real version) | 1 | :150-153 |
6.5 CompleteAttendanceDto / ReopenAttendanceDto / ResetAttendanceDto (requests)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
classPublicId | string (uuid) | Yes | N/A | @IsUUID | — | :157-159 |
date | string | Yes | N/A | @IsDateString | "2026-09-13" | :161-163 |
expectedVersion | number | null | Yes | N/A | @IsInt, @Min(0), nullable | 3 | :165-169 |
ReopenAttendanceDto and ResetAttendanceDto extend CompleteAttendanceDto verbatim, with no additional fields (:172,180).
6.6 StudentAttendanceRosterEntryDto (response, nested)
| Field | Type | Required | Default | Validation | Example | Source |
|---|---|---|---|---|---|---|
studentId | string (uuid) | Yes | N/A | — | — | :185 |
rollNumber | number | null | Yes | N/A | — | 12 | :188-189 |
admissionNumber | string | Yes | N/A | — | — | :190 |
studentCode | string | Yes | N/A | — | — | :191 |
fullName | string | Yes | N/A | Server-composed, collapsed whitespace | "Aashna Rai" | :192 |
recordId | string | null (uuid) | Yes | N/A | Null when unmarked | — | :193-194 |
status | "present"|"absent"|"late"|"excused"|null | Yes | N/A | Null when unmarked | — | :195-196 |
note | string | null | Yes | N/A | — | — | :197 |
markedAt | string | null (ISO) | Yes | N/A | Null when unmarked | — | :198 |
recordVersion | number | null | Yes | N/A | Null when unmarked; the token a subsequent PATCH must echo | 0 | :199-201 |
6.7 AttendanceTallyDto (response, nested)
| Field | Type | Source |
|---|---|---|
totalStudents, markedCount, unmarkedCount, presentCount, absentCount, lateCount, excusedCount | number | :204-212, all derived, never stored |
6.8 StudentAttendanceSessionDto / AttendanceCalendarDayDto (response, nested)
| Field | Type | Notes | Source |
|---|---|---|---|
id | string (uuid) | — | :215 |
status | "not_started"|"in_progress"|"completed" | — | :216-217 |
completedAt | string | null (ISO) | Non-null iff status = completed | :218 |
version | number | The session's own optimistic-concurrency token | :219 |
classification | one of the seven calendar_day_classification values | Owned by the academic-calendar module; consumed here | :228-229 |
attendanceExpected | boolean | Derived from classification, never stored | :230-233 |
eventName | string | null | e.g. "Dashain" | :234 |
6.9 StudentAttendanceStateDto (response, the shared envelope)
| Field | Type | Required | Notes | Source |
|---|---|---|---|---|
classPublicId | string (uuid) | Yes | — | :238 |
classLabel | string | Yes | e.g. "Grade 5 A (Morning)", composed server-side | :239-243 |
date | string | Yes | — | :244 |
calendar | AttendanceCalendarDayDto | Yes | — | :245-246 |
session | StudentAttendanceSessionDto | null | Yes | Null when never opened for this class/date — NOT a 404 | :247-248 |
version | number | null | Yes | Mirrors session?.version, flattened; null exactly when session is null | :249-252 |
roster | StudentAttendanceRosterEntryDto[] | Yes | — | :253-254 |
tally | AttendanceTallyDto | Yes | — | :255-256 |
This exact shape is returned by every read, write, and lifecycle endpoint under /attendance/students and /mobile/teacher/attendance — always a fresh re-read, never an echo of the request.
6.10 StudentAttendanceRecordDto (response, PATCH .../:recordId)
| Field | Type | Source |
|---|---|---|
id, sessionId, studentId | string (uuid) | :260-262 |
status | "present"|"absent"|"late"|"excused" | :263 |
note, forcedReason | string | null | :264-265 |
markedAt | string (ISO) | :266 |
version | number | :267 |
6.11 StudentAttendanceHistoryQueryDto / AttendanceHistoryEntryDto
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
from, to | string | No | N/A | @IsDateString, inclusive | dto/attendance-history.dto.ts:14-22 |
cursor | string | No | N/A | Opaque, base64url([changedAtIso, id]) | :24-29 |
size | number | No | 20 | @Min(1), @Max(100) | :31-36 |
id | string (uuid) | response | N/A | — | :41 |
attendanceDate | string | response | N/A | — | :42 |
previousStatus | status or null | response | N/A | Null means "was unmarked" — the FIRST marking, not only a correction | :43-49 |
newStatus | status or null | response | N/A | Null means "reset to unmarked" | :50-55 |
reason, classLabel | string | null | response | N/A | — | :56-57 |
changedByName | string | response | N/A | Snapshotted, survives the actor's own deletion | :58 |
changedAt | string (ISO) | response | N/A | — | :59 |
6.12 AttendanceOverviewQueryDto / AttendanceOverviewRowDto
| Field | Type | Required | Notes | Source |
|---|---|---|---|---|
date | string | Yes | @IsDateString | dto/attendance-overview.dto.ts:8-9 |
classPublicId | string (uuid) | response | — | :22 |
classLabel | string | response | — | :23 |
sessionStatus | session status or null | response | Null means no session row exists yet | :24-26 |
totalStudents, markedCount, presentCount, absentCount, lateCount, excusedCount | number | response | Grouped SQL, never a per-class round trip | :27-32 |
classTeacherName | string | null | response | — | :33 |
scheduleState | "published"|"no_published_timetable"|"no_period_today" | response | See §3 | :34-35 |
reminderSentAt | string | null (ISO) | response | — | :36 |
unalertedAbsences | number | response | Absent, past the cutoff, guardian_alerted_at IS NULL | :37-38 |
6.13 ListStaffAttendanceQueryDto
| Field | Type | Required | Default | Validation | Source |
|---|---|---|---|---|---|
date | string | Yes | N/A | @IsDateString | dto/staff-attendance.dto.ts:62-64 |
designationId | number | No | N/A | @IsInt — filters the Teachers/Staff tab | :66-72 |
page | number | No | 1 | @Min(1) | :74-79 |
size | number | No | 20 | @Min(1), @Max(100) | :81-87 |
6.14 StaffAttendanceRowDto / StaffAttendanceTallyDto / StaffAttendanceListDto (response)
| Field | Type | Notes | Source |
|---|---|---|---|
staffId | string (uuid) | — | :93 |
employeeCode, name | string | — | :94-95 |
designationName, departmentName | string | null | — | :96-97 |
status | status or null | Null when unmarked for the date | :98-99 |
note | string | null | — | :100 |
recordId | string | null (uuid) | Null when unmarked | :102-103 |
version | number | null | Null when unmarked; informational on the list — the bulk write carries no token at all | :104-107 |
(list) date | string | — | :121 |
(list) entries | StaffAttendanceRowDto[] | — | :122-123 |
(list) tally | StaffAttendanceTallyDto (totalStaff, markedCount, unmarkedCount, presentCount, absentCount, lateCount, excusedCount) | — | :110-126 |
6.15 StaffAttendanceEntryDto / BulkStaffAttendanceDto (request, PUT /attendance/staff)
| Field | Type | Required | Validation | Notes | Source |
|---|---|---|---|---|---|
staffId | string (uuid) | Yes | @IsUUID | — | :128-131 |
status | status or null | Yes | @IsIn(ATTENDANCE_STATUS), nullable | Null deletes the record for the date | :133-138 |
note | string | null | No | @MaxLength(500), format rule via custom @NoControlCharacters() decorator | — | :140-148 |
date | string | Yes | @IsDateString | — | :159-162 |
entries | StaffAttendanceEntryDto[] | Yes | @IsArray, @ValidateNested | No expectedVersion, no submissionId — this write is last-write-wins by design | :164-168 |
6.16 PatchStaffAttendanceDto (request, PATCH /attendance/staff/:recordId)
| Field | Type | Required | Validation | Notes | Source |
|---|---|---|---|---|---|
status | status | Yes | @IsIn(ATTENDANCE_STATUS) — never null; unmarking goes through the bulk endpoint | — | :178-180 |
note | string | null | No | Same format rule | — | :182-190 |
reason | string | null | No | Same format rule; written to the change log | — | :192-200 |
expectedVersion | number | Yes | @IsInt | This path DOES have a real row/token, unlike the bulk write | :202-208 |
6.17 StaffAttendanceHistoryQueryDto / StaffAttendanceHistoryEntryDto
Same shape and semantics as §6.11, applied to subjectKind = 'staff' (dto/staff-attendance.dto.ts:211-250).
6.18 AttendanceSettingRowDto / AttendanceSettingsListDto / UpdateAttendanceSettingsDto
| Field | Type | Required | Default | Validation | Notes | Source |
|---|---|---|---|---|---|---|
kind | "attendance.first_period_reminder"|"attendance.student_absent" | response | N/A | — | — | dto/attendance-settings.dto.ts:34-35 |
enabled | boolean | Yes (request) / response | N/A | @IsBoolean | — | :37,56 |
channels | string[] | Yes (request) / response | N/A | @ArrayMaxSize(4), @IsString({each:true}); membership against NOTIFICATION_CHANNEL and distinctness checked in the SERVICE, not here | Response typed as NotificationChannel[] | :39-40,63-67 |
version | number | response / request | N/A | @IsInt (request) | — | :42,69-71 |
effectiveNote | string | response | N/A | — | The subtractive-only explanation, always the same constant string | :44-45 |
expectedVersion | number | Yes (request) | N/A | @IsInt | — | :69-71 |
(list) settings | AttendanceSettingRowDto[] | response | N/A | — | Always both kinds, in ATTENDANCE_NOTIFICATION_KINDS order | :48-51 |
6.19 Portal DTOs
| DTO | Field | Type | Required | Notes | Source |
|---|---|---|---|---|---|
TeacherAttendanceQueryDto | date, classPublicId | string | Yes | — | dto/teacher-attendance-portal.dto.ts:14-22 |
TeacherMarkAttendanceDto | classPublicId, date, expectedVersion, submissionId, entries | (as §6.2, minus force/forcedReason) | Yes/Yes/Yes/Yes/Yes | Identical shape to BulkMarkAttendanceDto with the two admin-only fields REMOVED — sending either is a 400 via forbidNonWhitelisted | :30-54 |
TeacherCompleteAttendanceDto | classPublicId, date, expectedVersion | (as §6.5) | Yes/Yes/Yes | — | :56-70 |
TeacherClassSummaryDto | classPublicId, classLabel, totalStudents | response | Yes | — | :72-76 |
StudentAttendancePortalQueryDto | from, to | string | Yes | @IsDateString | dto/student-attendance-portal.dto.ts:6-14 |
StudentAttendancePortalEntryDto | attendanceDate, status, classLabel, bsYear, bsMonth, bsDay, note | response | Yes | No markedBy, no staff identity | :18-38 |
GuardianAttendancePortalQueryDto | from, to | string | Yes | @IsDateString | dto/guardian-attendance-portal.dto.ts:6-14 |
GuardianAttendancePortalEntryDto | attendanceDate, status, classLabel, bsYear, bsMonth, bsDay, note | response | Yes | No markedBy, no staff identity | :18-38 |
PortalAttendanceSummaryDto (response of both summary routes; portal/shared/portal-attendance-summary.dto.ts):
| Field | Type | Required | Notes |
|---|---|---|---|
academicSessionName | string | null | Yes (nullable) | Name of the current academic session, for example "2082/83". null when no session is current. |
from | string | null | Yes (nullable) | First day of the window, YYYY-MM-DD: the session's start date. |
to | string | null | Yes (nullable) | Last day of the window: school-local today, or the session's end date once the session has ended. |
expectedDays | number | Yes | Days in the window on which the pupil held an enrolment in a class and the calendar requires attendance (school_day, special_school_day). |
marked | number | Yes | Expected days that have a student attendance record for the pupil. |
present | number | Yes | Marked days with status present. |
late | number | Yes | Marked days with status late. |
absent | number | Yes | Marked days with status absent. |
excused | number | Yes | Marked days with status excused. |
unmarked | number | Yes | max(expectedDays - marked, 0). |
ratePct | number | null | Yes (nullable) | (present + late) as a percentage of marked, rounded to two decimals. A share strictly between none and all is clamped to 0.01–99.99, so a single absence never reads 100. null when nothing is marked. |
With no current session, or a window that has not opened yet (to earlier than the session start), every count is 0 and from, to and ratePct are null; academicSessionName is still set when a session exists.
7. Enum Reference
| Enum | Value | Meaning | Runtime Effect | Source |
|---|---|---|---|---|
attendance_status | present | Pupil/staff was present. | — | attendance.ts:60-65 |
attendance_status | absent | Pupil/staff was absent. | Qualifies for the guardian absence sweep (students only). | attendance.ts:60-65 |
attendance_status | late | Pupil/staff arrived late. | Never alerted by the absence sweep. | attendance.ts:60-65 |
attendance_status | excused | Pupil/staff was excused. | Never alerted by the absence sweep. | attendance.ts:60-65 |
attendance_session_status | not_started | No record has been written for the register yet. | — | attendance.ts:75-79 |
attendance_session_status | in_progress | At least one record exists; not yet completed. | Eligible for complete. | attendance.ts:75-79 |
attendance_session_status | completed | Every live roster pupil was marked and the office closed the day. | Bulk write requires refuseIfCompleted opt-out on the admin surface; the portal always refuses. | attendance.ts:75-79 |
attendance_subject_kind | student | A change-log row about a pupil. | — | attendance.ts:81-84 |
attendance_subject_kind | staff | A change-log row about a staff/teacher. | — | attendance.ts:81-84 |
AttendanceScheduleState | published | The class has a published timetable with a period today. | Reminder sweep can find a candidate. | dto/attendance-overview.dto.ts:12-18 |
AttendanceScheduleState | no_published_timetable | The class has no published timetable at all. | Reminder sweep will never find a candidate for this class. | same |
AttendanceScheduleState | no_period_today | The class has a published timetable but no period today. | Reminder sweep skips today only. | same |
AttendanceNotificationKind | attendance.first_period_reminder | The class-teacher reminder. | push,in_app,sms,email builders all exist. | attendance.types.ts:30-33, template-registry.ts:275 |
AttendanceNotificationKind | attendance.student_absent | The guardian alert. | Same four channel builders. | attendance.types.ts:30-33, template-registry.ts:327 |
8. Endpoint Reference
8.1 GET /api/attendance/students
Purpose
Returns a class's full register for one date — every live pupil, marked or not — for the admin attendance screen to render. A session: null in the response is normal for a class/date never opened and is not a 404.
Source Evidence
| Evidence | Path |
|---|---|
| Controller | apps/api/src/modules/attendance/admin/student-attendance.controller.ts:56-71 |
| DTO | apps/api/src/modules/attendance/admin/dto/student-attendance.dto.ts:117-125 |
| Service | apps/api/src/modules/attendance/admin/student-attendance-read.service.ts:139-192 |
| Schema | packages/db/src/schema/school/attendance.ts |
| Tests | apps/api/src/modules/attendance/__tests__/student-attendance.integration.spec.ts |
Auth and Permissions
- Auth: JWT required.
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission:
StudentAttendance_READ. - Guest support: No.
- Rate limit: Global default only.
- Idempotency: N/A (read).
Request
| Part | Required | Details |
|---|---|---|
| Headers | Yes | Authorization: Bearer <token>. |
| Params | No | — |
| Query | Yes | date (YYYY-MM-DD), classPublicId (uuid). |
| Body | No | — |
Response
200, StudentAttendanceStateDto (§6.9):
{
"success": true,
"message": "Attendance fetched.",
"data": {
"classPublicId": "018f...",
"classLabel": "Grade 5 A (Morning)",
"date": "2026-09-13",
"calendar": { "classification": "school_day", "attendanceExpected": true, "eventName": null },
"session": { "id": "018f...", "status": "in_progress", "completedAt": null, "version": 2 },
"version": 2,
"roster": [
{ "studentId": "018f...", "rollNumber": 1, "admissionNumber": "A-1024", "studentCode": "S-2091", "fullName": "Aashna Rai", "recordId": "018f...", "status": "present", "note": null, "markedAt": "2026-09-13T02:35:00.000Z", "recordVersion": 0 },
{ "studentId": "018f...", "rollNumber": 2, "admissionNumber": "A-1025", "studentCode": "S-2092", "fullName": "Bibek Thapa", "recordId": null, "status": null, "note": null, "markedAt": null, "recordVersion": null }
],
"tally": { "totalStudents": 2, "markedCount": 1, "unmarkedCount": 1, "presentCount": 1, "absentCount": 0, "lateCount": 0, "excusedCount": 0 }
}
}Side Effects
None — read-only. No cache read/write, no job, no notification, no audit write beyond the global request log.
Error Cases
| HTTP Status | Error Code | Condition | User-Facing Meaning | Source |
|---|---|---|---|---|
404 | CLASS_NOT_FOUND | classPublicId does not resolve. | Verify the class id. | student-attendance-read.service.ts:89-94 |
422 | ATTENDANCE_DATE_OUTSIDE_SESSION | date is outside the class's own academic session [start_date, end_date), whichever session is current. | The class belongs to a different academic year than the date; pick a class of the session that contains the date. | calendar-day.service.ts (requireWithinSession) |
409 | ACADEMIC_CALENDAR_NOT_GENERATED | date is inside the class's session but the calendar has no row for it. | Generate the calendar for this date first. | calendar-day.service.ts (require) |
Edge Cases
- Never-opened register:
session: null,version: null, every roster rowstatus: null. - A pupil with no assigned roll number sorts last, ordered after by admission number.
- Empty class (zero active enrolments):
roster: [],tally.totalStudents: 0.
Example Requests
GET /api/attendance/students?date=2026-09-13&classPublicId=018f2c4a-... HTTP/1.1
Authorization: Bearer TOKENcurl -G "$API_URL/api/attendance/students" \
-H "Authorization: Bearer TOKEN" \
-d date=2026-09-13 \
-d classPublicId=018f2c4a-...8.2 PUT /api/attendance/students
Purpose
Marks or corrects a class's register for one date, as a set of changes rather than a full-state rewrite. Opens the register lazily if this is the first write of the day. Call this from the class register screen on every "save" action.
Source Evidence
| Evidence | Path |
|---|---|
| Controller | student-attendance.controller.ts:73-89 |
| DTO | dto/student-attendance.dto.ts:44-115 |
| Service | admin/student-attendance-write.service.ts:87-367 |
| Schema | attendance.ts:90-207,209-315 |
| Tests | student-attendance.integration.spec.ts:277-525 |
Auth and Permissions
- Auth: JWT required.
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission:
StudentAttendance_CREATEANDStudentAttendance_UPDATE. - Guest support: No.
- Rate limit: Global default only.
- Idempotency: Yes —
submissionId; a replay writes nothing and returns200with the current state.
Request
| Part | Required | Details |
|---|---|---|
| Headers | Yes | Authorization: Bearer <token>. |
| Params | No | — |
| Query | No | — |
| Body | Yes | BulkMarkAttendanceDto (§6.2). |
Minimal valid body:
{
"classPublicId": "018f2c4a-...",
"date": "2026-09-13",
"expectedVersion": null,
"submissionId": "018f2c4b-...",
"entries": [
{ "studentId": "018f2c4c-...", "status": "present" }
]
}Full body with a force-write:
{
"classPublicId": "018f2c4a-...",
"date": "2026-09-14",
"expectedVersion": 0,
"submissionId": "018f2c4d-...",
"entries": [
{ "studentId": "018f2c4c-...", "status": "present", "note": "Makeup session" },
{ "studentId": "018f2c4e-...", "status": null }
],
"force": true,
"forcedReason": "Emergency makeup class on a public holiday"
}Response
200, StudentAttendanceStateDto (same shape as §8.1).
Side Effects
- Database writes:
student_attendance_sessions(insert-or-find),student_attendance_records(upsert marked entries, delete unmarked entries),attendance_change_log(one row per real status change, including the first marking),student_attendance_session_runtime(last_submission_id, and a status-advanceUPDATEonstudent_attendance_sessionswhen the first record is written). - Cache: None.
- Jobs: None enqueued synchronously.
- Notifications: None synchronous — the reminder sweep may later see this session already opened and skip its own candidate check.
- Audit:
ActivityAuditInterceptorrecords the call underStudentAttendance/CREATE(the first declared permission).
Error Cases
| HTTP Status | Error Code | Condition | User-Facing Meaning | Source |
|---|---|---|---|---|
422 | ATTENDANCE_EMPTY_PAYLOAD | entries is empty. | Submit at least one entry. | student-attendance-write.service.ts (bulkMark) |
422 | ATTENDANCE_DUPLICATE_STUDENT | The same studentId appears twice. | De-duplicate the payload. | student-attendance-write.service.ts (bulkMark) |
422 | ATTENDANCE_DATE_IN_FUTURE | date is after today, school-local. | Resend with a non-future date. | student-attendance-write.service.ts (bulkMark) |
422 | ATTENDANCE_DATE_OUTSIDE_SESSION | date is outside the class's academic session range. | Resend with a valid date. | calendar-day.service.ts (requireWithinSession) |
409 | ACADEMIC_CALENDAR_NOT_GENERATED | No calendar row for this date. | Generate the calendar first. | calendar-day.service.ts (require) |
422 | ATTENDANCE_NOT_A_SCHOOL_DAY | Not a school day and force unset. | Resend with force: true and a reason. | student-attendance-write.service.ts (bulkMark) |
409 | ATTENDANCE_VERSION_CONFLICT | Stale expectedVersion, or null when the register was already opened. | Reload and retry. | student-attendance-write.service.ts (bulkMark) |
422 | ATTENDANCE_STUDENT_NOT_ENROLLED | A studentId has no active enrolment in this class. | Remove it from the payload. | student-attendance-write.service.ts (bulkMark) |
500 | WRITE_RETURNED_NO_ROW | The session insert-then-reselect found nothing (practically unreachable). | Retry; report if persistent. | student-attendance-write.service.ts (bulkMark) |
409 | ACADEMIC_SESSION_LOCKED | The class's academic session has locked_at set. | Reopen the year before writing. | Postgres trigger, not this service — see the academic-sessions backend doc. |
Edge Cases
- Replayed
submissionId: no error, no new side effect, returns the current state. force: truewithforcedReasonomitted: accepted;forced_reasonstays null.- A single entry with
status: nullon a class that was never marked: no session existed, none created (the delete matches nothing), no error. - Every entry omits a pupil currently on the roll: that pupil is simply left unchanged — never implicitly unmarked.
Example Requests
PUT /api/attendance/students HTTP/1.1
Authorization: Bearer TOKEN
Content-Type: application/jsoncurl -X PUT "$API_URL/api/attendance/students" \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"classPublicId":"018f2c4a-...","date":"2026-09-13","expectedVersion":null,"submissionId":"018f2c4b-...","entries":[{"studentId":"018f2c4c-...","status":"present"}]}'8.3 PATCH /api/attendance/students/:recordId
Purpose
Corrects one pupil's already-existing record, keyed on the record's own version rather than the session's. Use this for a single fix after the fact — including against a completed register, which this route is deliberately permitted to touch.
Source Evidence
| Evidence | Path |
|---|---|
| Controller | student-attendance.controller.ts:91-108 |
| DTO | dto/student-attendance.dto.ts:130-154 |
| Service | admin/student-attendance-write.service.ts:372-478 |
| Tests | student-attendance.integration.spec.ts |
Auth and Permissions
- Auth: JWT required.
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission:
StudentAttendance_UPDATE. - Guest support: No.
- Idempotency: No — a repeated identical PATCH still requires the current
expectedVersionand bumpsversionagain.
Request
| Part | Required | Details |
|---|---|---|
| Params | Yes | recordId (uuid, path). |
| Body | Yes | PatchStudentAttendanceRecordDto (§6.4). |
{ "status": "excused", "note": "Doctor's appointment", "reason": "Parent provided a note after the fact", "expectedVersion": 0 }Response
200, StudentAttendanceRecordDto (§6.10).
Side Effects
- Writes:
student_attendance_records(the one row),attendance_change_log(one row,reasoncarried from the DTO). - No cache, job, or notification effects.
Error Cases
| HTTP Status | Error Code | Condition | Source |
|---|---|---|---|
404 | ATTENDANCE_RECORD_NOT_FOUND | recordId does not exist. | :388-393,439-442 |
404 | ATTENDANCE_SESSION_NOT_FOUND | The record's session is missing (should not occur under normal FK integrity). | :403-408 |
409 | ATTENDANCE_VERSION_CONFLICT | expectedVersion mismatched. | :444-448 |
409 | ACADEMIC_SESSION_LOCKED | The record's academic session has locked_at set. | Postgres trigger — see the academic-sessions backend doc. |
Edge Cases
- Correcting a
completedregister succeeds — this is the correction path for a closed day. reasonis written only to the change log, never to the record itself.
Example Requests
curl -X PATCH "$API_URL/api/attendance/students/018f2c4c-.../history" \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
-d '{"status":"excused","expectedVersion":0}'8.4 POST /api/attendance/students/complete
Purpose
Closes a register once every pupil on the current roll has a record. Called from the "finish taking attendance" action.
Source Evidence
student-attendance.controller.ts:110-125; admin/student-attendance-lifecycle.service.ts:57-102; dto/student-attendance.dto.ts:156-170.
Auth and Permissions
StudentAttendance_UPDATE. Same guard chain as above.
Request
Body: CompleteAttendanceDto (§6.5) — classPublicId, date, expectedVersion.
Response
200, StudentAttendanceStateDto with session.status: "completed".
Side Effects
Writes student_attendance_sessions only (status, completed_at, completed_by_user_id). No record/log write.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | ATTENDANCE_SESSION_NOT_FOUND | No register exists for the class/date. |
409 | ATTENDANCE_VERSION_CONFLICT | Stale token. |
409 | ATTENDANCE_ALREADY_COMPLETED | Already completed. |
422 | ATTENDANCE_INCOMPLETE_ROSTER | A live roster row has no record. |
Edge Cases
A pupil enrolled after the register was opened but before completion is included in the same-transaction roster read, so completion still correctly requires them to be marked.
8.5 POST /api/attendance/students/reopen
Purpose
Returns a completed register to in_progress so it can be bulk-corrected via PUT again.
Source Evidence
student-attendance.controller.ts:127-139; admin/student-attendance-lifecycle.service.ts:104-137.
Auth and Permissions
StudentAttendance_UPDATE.
Request
Body: ReopenAttendanceDto (identical shape to CompleteAttendanceDto).
Response
200, StudentAttendanceStateDto with session.status: "in_progress", completedAt: null.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | ATTENDANCE_SESSION_NOT_FOUND | No register exists. |
409 | ATTENDANCE_VERSION_CONFLICT | Stale token. |
409 | ATTENDANCE_NOT_COMPLETED | The register was never completed. |
8.6 POST /api/attendance/students/reset
Purpose
Clears a class's entire register for one date — deletes every record, logs one deletion per previously-marked pupil, and returns the session to not_started. The most destructive route in this module; use it to recover from a mismarked day, not for a routine correction (which is PATCH .../:recordId).
Source Evidence
student-attendance.controller.ts:141-157; admin/student-attendance-lifecycle.service.ts:155-215.
Auth and Permissions
StudentAttendance_DELETE — a distinct permission code from the mark/correct/complete/reopen paths, which all use _UPDATE.
Request
Body: ResetAttendanceDto (identical shape to CompleteAttendanceDto). expectedVersion is required here for the same reason it is required on complete and reopen — a stale screen must not be able to erase marks someone else just saved.
Response
200, StudentAttendanceStateDto with session.status: "not_started", roster entirely unmarked, tally.markedCount: 0.
Side Effects
- Writes:
DELETEon everystudent_attendance_recordsrow for the session; oneattendance_change_logrow per deleted record (newStatus: null);student_attendance_sessionsreset;student_attendance_session_runtime.last_submission_idcleared.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | ATTENDANCE_SESSION_NOT_FOUND | No register exists. |
409 | ATTENDANCE_VERSION_CONFLICT | Stale token. |
Edge Cases
Resetting a register with nothing marked yet: succeeds as a near no-op (session status write only; nothing to delete or log).
8.7 GET /api/attendance/students/:studentId/history
Purpose
A pupil's full attendance change history, including the first marking of every day, keyset-paginated. Use this for a pupil's attendance detail/history screen.
Source Evidence
student-attendance-history.controller.ts:20-45; admin/attendance-history.service.ts; dto/attendance-history.dto.ts.
Auth and Permissions
StudentAttendance_READ.
Request
| Part | Required | Details |
|---|---|---|
| Params | Yes | studentId (uuid). |
| Query | No | from, to (date, inclusive), cursor (opaque), size (1–100, default 20). |
Response
200:
{
"success": true,
"message": "History fetched.",
"data": [
{ "id": "018f...", "attendanceDate": "2026-09-13", "previousStatus": null, "newStatus": "present", "reason": null, "classLabel": "Grade 5 A (Morning)", "changedByName": "Sunita Adhikari", "changedAt": "2026-09-13T02:35:00.123Z" }
],
"nextCursor": "eyJ..."
}data is the array; nextCursor is a top-level envelope field, null on the last page.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
400 | PAGINATION_CURSOR_INVALID | Malformed cursor. |
Edge Cases
- A day the pupil was marked once, never corrected: one entry,
previousStatus: null. - A bulk mark of many pupils on one day, viewed for one pupil: their single entry among the tied
changed_attimestamps is still returned exactly once, at the correct page boundary. - Empty result (no history yet):
data: [],nextCursor: null.
8.8 GET /api/attendance/overview
Purpose
The whole school's attendance state for one date, one row per active class in the academic session containing that date — the office's "who hasn't taken the register yet" screen.
Source Evidence
attendance-overview.controller.ts; attendance-overview.service.ts; dto/attendance-overview.dto.ts.
Auth and Permissions
StudentAttendance_READ.
Request
Query: date (required).
Response
200, array of AttendanceOverviewRowDto (§6.12). Empty array when no active session contains the date, or the session has no active classes.
Error Cases
None specific — an unresolvable date returns [] rather than erroring.
Edge Cases
- A class with a published timetable but no period on that weekday:
scheduleState: "no_period_today". - A class never opened for the date:
sessionStatus: null, all counts0. unalertedAbsencescan be nonzero even after the sweep has run, briefly, if the sweep tick has not yet reached that class's records.
8.9 GET /api/attendance/staff
Purpose
Lists the staff/teacher register for one date, optionally filtered by designationId for the Teachers/Staff tabs, paginated.
Source Evidence
staff-attendance.controller.ts:46-70; staff-attendance.service.ts:98-169.
Auth and Permissions
StaffAttendance_READ.
Request
Query: date (required), designationId (optional), page, size (default 1/20).
Response
200, StaffAttendanceListDto (§6.14), with pagination metadata (count, page, size) via PaginationUtil.
Side Effects
None — read-only.
Edge Cases
An unmarked staff member appears in the list with status: null, recordId: null — never omitted.
8.10 PUT /api/attendance/staff
Purpose
Marks the staff/teacher register for one date, including on a holiday or exam day (the calendar gate is never consulted here). Last write for the date wins — there is no concurrency token on this path.
Source Evidence
staff-attendance.controller.ts:72-90; staff-attendance.service.ts:183-311; dto/staff-attendance.dto.ts:159-169.
Auth and Permissions
StaffAttendance_CREATE AND StaffAttendance_UPDATE.
Request
Body: BulkStaffAttendanceDto — date, entries[] (§6.15). No expectedVersion, no submissionId.
{ "date": "2026-09-13", "entries": [{ "staffId": "018f...", "status": "present" }] }Response
200, { date, entries: StaffAttendanceRowDto[], tally: StaffAttendanceTallyDto }.
Side Effects
Writes: staff_attendance_records (upsert marked, delete status: null), attendance_change_log (one row per real change, including the first marking).
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
422 | ATTENDANCE_EMPTY_PAYLOAD | entries empty. |
422 | ATTENDANCE_DUPLICATE_STUDENT | Same staffId twice. |
422 | ATTENDANCE_DATE_IN_FUTURE | Future date. |
422 | ATTENDANCE_DATE_OUTSIDE_SESSION | Date not inside any academic session. |
404 | STAFF_ATTENDANCE_RECORD_NOT_FOUND | A staffId does not resolve. |
Edge Cases
- Marking on a public holiday: succeeds.
- Two administrators submitting the same day simultaneously: whichever transaction commits last wins entirely; both are individually logged, with no conflict surfaced to either caller.
8.11 PATCH /api/attendance/staff/:recordId
Purpose
Corrects one staff attendance record, using that record's own version — unlike the bulk write, this path has a real row to check optimistically.
Source Evidence
staff-attendance.controller.ts:92-107; staff-attendance.service.ts:315-404; dto/staff-attendance.dto.ts:171-209.
Auth and Permissions
StaffAttendance_UPDATE.
Request
Params: recordId. Body: PatchStaffAttendanceDto (§6.16).
Response
200, StaffAttendanceRowDto — re-read after the write, never an echo.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | STAFF_ATTENDANCE_RECORD_NOT_FOUND | recordId does not exist. |
409 | ATTENDANCE_VERSION_CONFLICT | expectedVersion mismatched. |
8.12 GET /api/attendance/staff/:staffId/history
Purpose
One staff member's attendance change history, keyset-paginated identically to the student history route.
Source Evidence
staff-attendance-history.controller.ts; staff-attendance.service.ts:414-490; dto/staff-attendance.dto.ts:211-250.
Auth and Permissions
StaffAttendance_READ.
Request
Params: staffId. Query: from, to, cursor, size.
Response
200, { data: StaffAttendanceHistoryEntryDto[], nextCursor } — same envelope shape as §8.7, built from Postgres's own ::text rendering of changed_at rather than date_trunc, closing the identical precision gap by a different route.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
400 | PAGINATION_CURSOR_INVALID | Malformed cursor. |
8.13 GET /api/attendance/settings
Purpose
Reads both attendance notification settings rows, for the settings screen.
Source Evidence
attendance-settings.controller.ts:43-55; attendance-settings.service.ts:39-71.
Auth and Permissions
AttendanceSettings_READ.
Response
200, AttendanceSettingsListDto — always both kinds, each carrying effectiveNote.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | ATTENDANCE_SETTINGS_NOT_FOUND | Either seeded row is missing (an operational fault, not a normal client path). |
8.14 PUT /api/attendance/settings/:kind
Purpose
Updates which channels one attendance notification kind may use. Never creates a row — both are seeded by migration and this endpoint only edits them.
Source Evidence
attendance-settings.controller.ts:57-73; attendance-settings.service.ts:73-146; dto/attendance-settings.dto.ts:53-71.
Auth and Permissions
AttendanceSettings_UPDATE.
Request
Params: kind (attendance.first_period_reminder | attendance.student_absent). Body: UpdateAttendanceSettingsDto.
{ "enabled": true, "channels": ["push", "in_app", "sms", "email"], "expectedVersion": 0 }Response
200, AttendanceSettingRowDto, re-read after the write.
Side Effects
Writes attendance_notification_settings. No downstream effect on any already-sent notification.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
404 | ATTENDANCE_SETTINGS_KIND_UNKNOWN | :kind is not one of the two known values. |
404 | ATTENDANCE_SETTINGS_NOT_FOUND | The row is missing (operational fault). |
422 | ATTENDANCE_CHANNEL_UNKNOWN | A channel is not a known NOTIFICATION_CHANNEL, or repeats. |
409 | ATTENDANCE_VERSION_CONFLICT | Stale expectedVersion. |
Edge Cases
Enabling email/sms here does not notify any recipient who has never opened their own notification preferences — CATEGORY_DEFAULTS.attendance still applies afterwards, defaulting both off.
8.15 GET /api/mobile/teacher/attendance/classes
Purpose
Lists the classes the calling teacher is the class teacher of, with occupancy counts, for the teacher's class-picker screen.
Source Evidence
teacher-attendance-portal.controller.ts:41-51; teacher-attendance-portal.service.ts:42-83.
Auth and Permissions
- Auth: JWT required.
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission: None — audience-asserted via
assertTeacherStaffRow. - Guest support: No.
Response
200, TeacherClassSummaryDto[] — empty array if the caller teaches no active class in the current academic session.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | No active role, an active role that is guardian/student, or no live staff row for this user. |
8.16 GET /api/mobile/teacher/attendance
Purpose
Reads one of the teacher's own classes' register for one date — identical response shape to the admin GET, scoped to a class they actually teach.
Source Evidence
teacher-attendance-portal.controller.ts:53-64; teacher-attendance-portal.service.ts:85-94.
Auth and Permissions
No @Permissions(); assertTeacherStaffRow plus assertOwnClass.
Request
Query: date, classPublicId (§6.19).
Response
200, StudentAttendanceStateDto (§6.9) — same shape as §8.1.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | Not a staff session. |
403 | ATTENDANCE_NOT_CLASS_TEACHER | Not the class teacher of this class. |
404 | CLASS_NOT_FOUND | classPublicId unresolvable. |
409 | ACADEMIC_CALENDAR_NOT_GENERATED | No calendar row for this date. |
422 | ATTENDANCE_DATE_OUTSIDE_SESSION | date is outside the class's academic session range. |
8.17 PUT /api/mobile/teacher/attendance
Purpose
Marks the teacher's own class's register — today only, and refused once completed, since correcting a closed day is the admin surface's job.
Source Evidence
teacher-attendance-portal.controller.ts:66-81; teacher-attendance-portal.service.ts:99-121; dto/teacher-attendance-portal.dto.ts:30-53.
Auth and Permissions
No @Permissions(); assertTeacherStaffRow plus assertOwnClass.
Request
Body: TeacherMarkAttendanceDto (§6.19) — identical to BulkMarkAttendanceDto minus force/forcedReason; sending either is a 400 via forbidNonWhitelisted.
Response
200, StudentAttendanceStateDto.
Side Effects
Identical to §8.2 — this route calls the exact same StudentAttendanceWriteService.bulkMark transaction with { refuseIfCompleted: true, requireToday: true }.
Error Cases
Same as §8.2, plus:
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | ATTENDANCE_NOT_CLASS_TEACHER | Not this class's teacher. |
422 | ATTENDANCE_DATE_NOT_TODAY | date is not today, school-local. |
409 | ATTENDANCE_ALREADY_COMPLETED | The register is already completed. |
400 | (validation) | force/forcedReason present in the payload. |
Edge Cases
A teacher submitting exactly at the school-local midnight boundary is evaluated against SchoolClockService.now(), not the request's own clock.
8.18 POST /api/mobile/teacher/attendance/complete
Purpose
Completes the teacher's own class's register once every pupil is marked.
Source Evidence
teacher-attendance-portal.controller.ts:83-94; teacher-attendance-portal.service.ts:123-135.
Auth and Permissions
No @Permissions(); assertTeacherStaffRow plus assertOwnClass.
Request
Body: TeacherCompleteAttendanceDto (§6.19).
Response
200, StudentAttendanceStateDto with session.status: "completed".
Error Cases
Same as §8.4, plus 403 ATTENDANCE_NOT_CLASS_TEACHER.
8.19 GET /api/mobile/student/attendance
Purpose
A pupil's own attendance, date-ranged, for their own attendance-history screen.
Source Evidence
student-attendance-portal.controller.ts; student-attendance-portal.service.ts; dto/student-attendance-portal.dto.ts.
Auth and Permissions
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission: None —
assertPortalAudience(actor, "student"). - Takes no id parameter; the pupil is
students.user_id = actor.id.
Request
Query: from, to (both required, @IsDateString).
Response
200, StudentAttendancePortalEntryDto[] (§6.19) — no markedBy, no staff identity.
{
"success": true,
"message": "Attendance fetched.",
"data": [
{ "attendanceDate": "2026-09-13", "status": "present", "classLabel": "Grade 5 A (Morning)", "bsYear": 2083, "bsMonth": 5, "bsDay": 29, "note": null }
]
}Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | Active role's scopeKind is not student. |
404 | STUDENT_NOT_FOUND | The caller's own students row does not resolve. |
Edge Cases
A date outside the Bikram Sambat converter's supported range (2000–2090 BS) returns bsYear/bsMonth/bsDay: 0/0/0 for that row rather than failing the page — unreachable in practice for any date attendance could have been recorded on.
8.20 GET /api/mobile/guardian/children/:studentId/attendance
Purpose
One child's attendance, date-ranged, as read by their guardian. Used by the guardian's per-child attendance screen.
Source Evidence
guardian-attendance-portal.controller.ts; guardian-attendance-portal.service.ts; dto/guardian-attendance-portal.dto.ts.
Auth and Permissions
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission: None —
assertPortalAudience(actor, "guardian")plus a ward check.
Request
Params: studentId (uuid). Query: from, to.
Response
200, GuardianAttendancePortalEntryDto[] — same shape as §8.19, no markedBy.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | Active role's scopeKind is not guardian. |
404 | STUDENT_NOT_FOUND | studentId is not this guardian's ward (identical response to a genuinely nonexistent id), or the child's graduate access has ended — an ended graduation answers identically to a non-ward, never a distinguishing status. |
Edge Cases
A guardian with two children can never distinguish, from this route's response, whether a 404 means "wrong id" or "not your child" — that ambiguity is deliberate, so the id space cannot be probed as an enumeration oracle.
8.21 GET /api/mobile/student/attendance/summary
Purpose
A pupil's attendance totals for the current academic session, from the session start to today. Feeds the pupil dashboard.
Source Evidence
student-attendance-portal.controller.ts (mySummary); student-attendance-portal.service.ts (resolveOwnStudentId); portal/shared/portal-attendance-summary.service.ts; portal/shared/portal-attendance-summary.dto.ts; apps/api/src/modules/reports/portal/queries/portal-attendance.query.ts; apps/api/src/common/utils/cache-race.util.ts.
Auth and Permissions
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission: None —
assertPortalAudience(actor, "student"). The handler keyStudentAttendancePortalController.mySummaryis inNO_PERMISSION_ADMIN_HANDLERS. - The route takes no id. The pupil is resolved from the token (
students.user_id = actor.id,deleted_atonly).
Request
No params, query or body.
Response
{
"message": "Attendance summary fetched.",
"data": {
"academicSessionName": "2082/83",
"from": "2026-04-14",
"to": "2026-10-06",
"expectedDays": 120,
"marked": 118,
"present": 104,
"late": 6,
"absent": 5,
"excused": 3,
"unmarked": 2,
"ratePct": 93.22
},
"errorCode": null
}Fields are described under Portal DTOs in §6.19.
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | Active role's scopeKind is not student. |
404 | STUDENT_NOT_FOUND | The caller has no live students row. |
Edge Cases
- The window ends at school-local today, never at the session's end date, so a required day that has not happened yet is not counted as expected. The end date is inclusive.
- A pupil is expected only on days inside an enrolment span in the current session (
enrolled_on <= dayandended_onnull or after the day), so a mid-year transfer counts each class for its own days. ratePctisnullwhenmarkedis0.unmarkedcounts expected days that have no mark yet, so a day the teacher has not registered is not treated as absent.- The totals are cached for 60 seconds per pupil and school-local date, so a mark made in the register can take up to 60 seconds to appear. See §11.
Example Requests
curl -X GET "$API_URL/api/mobile/student/attendance/summary" \
-H "Authorization: Bearer TOKEN"8.22 GET /api/mobile/guardian/children/:studentId/attendance/summary
Purpose
One child's attendance totals for the current academic session, as read by their guardian. Same payload as §8.21.
Source Evidence
guardian-attendance-portal.controller.ts (childSummary); guardian-attendance-portal.service.ts (resolveChildId, resolveWard); portal/shared/portal-attendance-summary.service.ts.
Auth and Permissions
- Guard chain:
JwtAuthGuard,RoleGuard. - Permission: None —
assertPortalAudience(actor, "guardian")plus the ward check. The handler keyGuardianAttendancePortalController.childSummaryis inNO_PERMISSION_ADMIN_HANDLERS. - The ward check runs first; the cache is read only for an id that passed it.
Request
Params: studentId (uuid; a non-UUID value is a 400 from ParseUUIDPipe). No query or body.
Response
200, PortalAttendanceSummaryDto — the same shape as §8.21, with message: "Attendance summary fetched.".
Error Cases
| HTTP Status | Error Code | Condition |
|---|---|---|
403 | PERMISSION_INSUFFICIENT | Active role's scopeKind is not guardian. |
404 | STUDENT_NOT_FOUND | studentId is not this guardian's ward, does not exist, or the child's graduate access has ended. All three answer identically. |
Edge Cases
- The cache entry is keyed by pupil, not by caller, and is reachable only after identity is resolved, so the pupil route and the guardian route share one entry.
- Windowing, expected-day and rate rules are those of §8.21.
Example Requests
curl -X GET "$API_URL/api/mobile/guardian/children/01922e2d-3c4d-7c3a-9d2e-1a2b3c4d5e6f/attendance/summary" \
-H "Authorization: Bearer TOKEN"9. Flow Diagrams
9.1 Route Ownership
9.2 Request Sequence — bulk mark (either surface)
9.3 Error Branch
10. Pagination, Sorting, Filtering, and Search
| Endpoint | Pagination Type | Default Size | Max Size | Sort Fields | Filters | Result Cap |
|---|---|---|---|---|---|---|
GET /attendance/students/:studentId/history | Keyset (cursor) | 20 | 100 | changed_at DESC, id DESC (fixed) | from, to (attendance date range) | None beyond page size |
GET /attendance/staff/:staffId/history | Keyset (cursor) | 20 | 100 | changed_at DESC, id DESC (fixed) | from, to | None beyond page size |
GET /attendance/staff | Offset (page/size) | 1/20 | 100 | Fixed: name, then id | designationId | PaginationUtil.MAX_PAGE_SIZE (100) |
No endpoint in this module supports free-text search or client-chosen sort fields. The staff list computes its full roster and tally before slicing to a page — the tally therefore always reflects the whole roster, never just the visible page. Both history routes reject a malformed cursor with 400 PAGINATION_CURSOR_INVALID rather than silently restarting; a caller should treat that specific code as "start over from page one."
11. Caching, Jobs, and External Integrations
| Integration | Used? | Details | Source |
|---|---|---|---|
| Redis cache | Only the two portal summary routes | portal:attendance-summary: keys, 60-second TTL, one entry per pupil and school-local date. The read goes through cacheGetFast, which races the Redis read against a 250 ms budget and treats a timeout, an error or an outage as a miss, so the route falls back to the database; the write is fire-and-forget (cacheSetFast). No writer invalidates the key: a register mark, a correction or a calendar edit is visible after at most 60 seconds. Every other route reads the database directly. | portal/shared/portal-attendance-summary.service.ts; common/utils/cache-race.util.ts |
| BullMQ | Yes | QueueName.ATTENDANCE, two jobs (FIRST_PERIOD_REMINDER, ABSENCE_ALERT), one worker, ticked every minute by AttendanceScheduler. See backend doc §9. | packages/jobs/src/index.ts:1029-1044, attendance-workers.module.ts |
| External API | No | Notification delivery is internal to NotificationModule; no external provider is called directly by this module. | — |
13. Mandatory Deep API Documentation Pack
13.1 Route-by-Route Completeness Matrix
| Route | Controller Method | DTOs | Service Method | Guards | Permissions | Cache | Jobs | DB Touches | Errors | Tests | Documented? |
|---|---|---|---|---|---|---|---|---|---|---|---|
GET /attendance/students | StudentAttendanceController.getAttendance | GetStudentAttendanceQueryDto→StudentAttendanceStateDto | StudentAttendanceReadService.getState | Jwt, Role | StudentAttendance_READ | N/A | N/A | sessions, records, roster tables | CLASS_NOT_FOUND, ATTENDANCE_DATE_OUTSIDE_SESSION, ACADEMIC_CALENDAR_NOT_GENERATED | student-attendance.integration.spec.ts | Yes |
PUT /attendance/students | .mark | BulkMarkAttendanceDto→StudentAttendanceStateDto | StudentAttendanceWriteService.bulkMark | Jwt, Role | StudentAttendance_CREATE,_UPDATE | N/A | N/A | sessions, records, log, runtime | see §8.2 | same | Yes |
PATCH /attendance/students/:recordId | .correct | PatchStudentAttendanceRecordDto→StudentAttendanceRecordDto | .patchRecord | Jwt, Role | StudentAttendance_UPDATE | N/A | N/A | records, log | see §8.3 | same | Yes |
POST /attendance/students/complete | .complete | CompleteAttendanceDto→StudentAttendanceStateDto | StudentAttendanceLifecycleService.complete | Jwt, Role | StudentAttendance_UPDATE | N/A | N/A | sessions | see §8.4 | same | Yes |
POST /attendance/students/reopen | .reopen | ReopenAttendanceDto→same | .reopen | Jwt, Role | StudentAttendance_UPDATE | N/A | N/A | sessions | see §8.5 | same | Yes |
POST /attendance/students/reset | .reset | ResetAttendanceDto→same | .reset | Jwt, Role | StudentAttendance_DELETE | N/A | N/A | records, log, sessions, runtime | see §8.6 | same | Yes |
GET /attendance/students/:studentId/history | StudentAttendanceHistoryController.getHistory | StudentAttendanceHistoryQueryDto→AttendanceHistoryEntryDto[] | AttendanceHistoryService.getHistory | Jwt, Role | StudentAttendance_READ | N/A | N/A | change log | PAGINATION_CURSOR_INVALID | same | Yes |
GET /attendance/overview | AttendanceOverviewController.getOverview | AttendanceOverviewQueryDto→AttendanceOverviewRowDto[] | AttendanceOverviewService.getOverview | Jwt, Role | StudentAttendance_READ | N/A | N/A | classes, sessions, records, runtime, timetables, timeslots | none specific | same | Yes |
GET /attendance/staff | StaffAttendanceController.list | ListStaffAttendanceQueryDto→StaffAttendanceListDto | StaffAttendanceService.list | Jwt, Role | StaffAttendance_READ | N/A | N/A | staff, records | none specific | staff-attendance.integration.spec.ts | Yes |
PUT /attendance/staff | .bulkWrite | BulkStaffAttendanceDto→list+tally | .bulkWrite | Jwt, Role | StaffAttendance_CREATE,_UPDATE | N/A | N/A | records, log | see §8.10 | same | Yes |
PATCH /attendance/staff/:recordId | .patch | PatchStaffAttendanceDto→StaffAttendanceRowDto | .patch | Jwt, Role | StaffAttendance_UPDATE | N/A | N/A | records, log | see §8.11 | same | Yes |
GET /attendance/staff/:staffId/history | StaffAttendanceHistoryController.history | StaffAttendanceHistoryQueryDto→entries | .history | Jwt, Role | StaffAttendance_READ | N/A | N/A | change log | PAGINATION_CURSOR_INVALID | same | Yes |
GET /attendance/settings | AttendanceSettingsController.list | none→AttendanceSettingsListDto | AttendanceSettingsService.list | Jwt, Role | AttendanceSettings_READ | N/A | N/A | settings | ATTENDANCE_SETTINGS_NOT_FOUND | attendance-settings.integration.spec.ts | Yes |
PUT /attendance/settings/:kind | .update | UpdateAttendanceSettingsDto→AttendanceSettingRowDto | .update | Jwt, Role | AttendanceSettings_UPDATE | N/A | N/A | settings | see §8.14 | same | Yes |
GET /mobile/teacher/attendance/classes | TeacherAttendancePortalController.listMyClasses | none→TeacherClassSummaryDto[] | .listMyClasses | Jwt, Role | none (audience) | N/A | N/A | staff, classes | PERMISSION_INSUFFICIENT | attendance-portal.integration.spec.ts | Yes |
GET /mobile/teacher/attendance | .getAttendance | TeacherAttendanceQueryDto→StudentAttendanceStateDto | .getAttendance | Jwt, Role | none (audience) | N/A | N/A | (delegates to admin read) | ATTENDANCE_NOT_CLASS_TEACHER | same | Yes |
PUT /mobile/teacher/attendance | .markAttendance | TeacherMarkAttendanceDto→same | .markAttendance | Jwt, Role | none (audience) | N/A | N/A | (delegates to admin write) | see §8.17 | same | Yes |
POST /mobile/teacher/attendance/complete | .completeAttendance | TeacherCompleteAttendanceDto→same | .completeAttendance | Jwt, Role | none (audience) | N/A | N/A | (delegates to admin lifecycle) | see §8.18 | same | Yes |
GET /mobile/student/attendance | StudentAttendancePortalController.myAttendance | StudentAttendancePortalQueryDto→entries | .myAttendance | Jwt, Role | none (audience) | N/A | N/A | students, records | STUDENT_NOT_FOUND | same | Yes |
GET /mobile/guardian/children/:studentId/attendance | GuardianAttendancePortalController.childAttendance | GuardianAttendancePortalQueryDto→entries | .childAttendance | Jwt, Role | none (audience) | N/A | N/A | guardians, students, records | STUDENT_NOT_FOUND | same | Yes |
GET /mobile/student/attendance/summary | StudentAttendancePortalController.mySummary | PortalAttendanceSummaryDto | StudentAttendancePortalService.resolveOwnStudentId, PortalAttendanceSummaryService.forStudent | Jwt, Role | none (audience) | portal:attendance-summary: (60 s) | N/A | students, academic_sessions, academic_calendar_days, student_class_enrollments, student_attendance_sessions/records | STUDENT_NOT_FOUND | portal-attendance-summary specs | Yes |
GET /mobile/guardian/children/:studentId/attendance/summary | GuardianAttendancePortalController.childSummary | PortalAttendanceSummaryDto | GuardianAttendancePortalService.resolveChildId, PortalAttendanceSummaryService.forStudent | Jwt, Role | none (audience) | portal:attendance-summary: (60 s) | N/A | guardians, students, users, academic_sessions, academic_calendar_days, student_class_enrollments, student_attendance_sessions/records | STUDENT_NOT_FOUND | portal-attendance-summary specs | Yes |
13.2 Request/Response Exhaustiveness
Covered per-endpoint in §8 above — every mutating route includes a minimal and (where the DTO has optional fields) a full example; every route documents its success response, its representative validation/domain errors, and (where applicable) its empty-result behavior.
13.3 API Diagram Pack
See §9 for the route-ownership graph, the bulk-mark sequence diagram, and the shared error decision tree — the same three shapes apply, with only the actor and the entry guard changing, across every mutating route in this module (admin PUT/PATCH/POST and the teacher portal's PUT/POST).
13.4 Consumer Integration Notes
| Consumer | Required Knowledge | Failure Handling | Contract Stability |
|---|---|---|---|
| Admin panel | expectedVersion/submissionId on every mutating student route; force/forcedReason for holidays; StudentAttendance_DELETE is a distinct permission from _UPDATE for reset. | Show ATTENDANCE_VERSION_CONFLICT as "someone else changed this — reload"; show ATTENDANCE_NOT_A_SCHOOL_DAY with a force-confirm dialog. | Stable. |
| Teacher-facing web/mobile client | The portal write is today-only and pre-completion-only; there is no force field to send. | On ATTENDANCE_DATE_NOT_TODAY/ATTENDANCE_ALREADY_COMPLETED, direct the user to the admin surface rather than retrying. | Stable. |
| Pupil/guardian-facing client | No marked_by field exists in either portal response — do not design a UI element that assumes one. | A 404 from the guardian route means either a bad id or a non-ward; treat both identically. | Stable. |
| QA | Both history routes require the id tiebreaker to test pagination correctly — construct a bulk mark of several pupils to reliably produce a changed_at tie. | Reproduce the append-only trigger by attempting a direct UPDATE/DELETE on attendance_change_log and expecting SQLSTATE 2F002. | Stable. |
| Internal service (workers) | Both sweeps are idempotent per tick via a guarded UPDATE ... WHERE ... IS NULL; neither throws on a domain skip. | A processor exception should be treated as retryable (BullMQ's own retry semantics apply); a logged skip is not a failure. | Stable. |
13.5 API Tradeoffs and Rationale
| Decision | Chosen Behavior | Alternatives Considered | Why This Tradeoff | Risk | Mitigation |
|---|---|---|---|---|---|
| Pagination style (history) | Keyset (cursor) | Offset (page/size) | An offset page over a table that grows continuously (the change log) shifts under a concurrent write; keyset is stable across inserts. | A client must persist the opaque cursor rather than compute page N directly. | nextCursor is always returned; there is no "jump to page N" use case for a history feed. |
| Pagination style (staff list) | Offset (page/size) | Keyset | The staff roster for one date is bounded and effectively static within a request; offset is simpler and the tally needs the whole roster anyway. | None material at staff-roster scale. | — |
| Auth mode (admin vs. portal) | Permission-gated admin routes; audience-asserted, permission-free portal routes | Grant a scoped permission to teacher/student/guardian roles | The seeded roles' scopeKind: "all" and TEACHER_PERMISSIONS = STAFF_PERMISSIONS reference make any such grant unscoped in practice. | A future change to the seeded role shape could silently widen a portal-equivalent permission — mitigated by the module never granting one at all. | The dual role.guard.ts/route-permissions.spec.ts allowlist. |
| Error shape | Existing envelope (success/message/data, errorCode inside the thrown exception body) | A dedicated attendance error envelope | Consistency with every other module in this API. | None. | — |
| Idempotency (student bulk write) | Client-generated submissionId, checked before the version check | Rely on natural upsert idempotency alone | A naturally-idempotent upsert still bumps version, breaking a genuinely concurrent second edit's version check. | None material — the extra column and read are cheap. | — |
| Idempotency (staff bulk write) | None — last-write-wins | The same submissionId mechanism as students | Staff attendance is single-office, low-contention; the machinery was judged not worth it. | A genuine double-submit from the same office silently reapplies (harmlessly, since the entries are identical) or, from two different offices, the later one wins. | Both writes are individually logged for audit. |
| Backward compatibility | Not yet exercised — this is the module's first release | — | — | — | — |
13.6 API Change Impact
| Change | Affected Consumers | Backend Impact | Data Impact | Migration Needed? | Compatibility Plan |
|---|---|---|---|---|---|
Adding a fifth attendance_status value | Every DTO enumerating ATTENDANCE_STATUS, both notification templates | Enum migration, ATTENDANCE_STATUS const update in attendance.types.ts | Existing rows unaffected; new status usable going forward | Yes — Postgres enum ALTER TYPE ... ADD VALUE | Additive; no removal, so old clients simply never send the new value. |
| Adding a channel to a notification kind's settings row | UpdateAttendanceSettingsDto, both processors | Update attendance_notification_settings_channels_array CHECK if the max-4 cap needs raising | None to existing rows | Possibly (CHECK bound) | Document the new channel in the settings screen before enabling it by default. |
Renaming a StudentAttendance_* permission code | Admin panel's permission list, activity-log filter (ACTIVITY_MODULES in the frontend repo) | Update permission-catalog.ts, re-run permissions:sync | Existing role grants keyed on the old string name would need remapping | Yes — a data migration over role_permissions | Requires a coordinated frontend change; see the cross-repo activity-modules-parity rule in this repo's CLAUDE.md. |
14. Zero-Omission API Checklist
- Every controller route is documented (§4, §8).
- Every parent route prefix and runtime URL is documented (§4).
- Every DTO field, nested field, enum, default, and validator is documented (§6, §7).
- Every response field, nullable field, and generated field is documented (§6.6–§6.19).
- Every auth, guard, permission, and audience-assertion branch is documented (§5, §8).
- Every success, validation, permission, not-found, conflict, and server-error branch is documented (§8's Error Cases tables).
- Every database touch, and the absence of any cache/queue/notification side effect where true, is documented (§8's Side Effects, §11).
- Every route has minimal/full-request and success/error examples where a body applies (§8).
- Every endpoint family has route, sequence, and error diagrams (§9).
- Every tradeoff is documented (§13.5).
- The API doc links to backend and features/flows (below).
15. Integration Checklist
- Every route from controllers is documented.
- Every DTO field is documented.
- Every enum value is documented.
- Every response envelope is documented.
- Every error code is documented.
- Every auth guard and permission (or its deliberate absence) is documented.
- Every cache key (none), queue job, and external call is documented.
- Every diagram matches the current code.
- The API doc links to backend and features/flows.
See Also
- Backend doc:
/docs/developer/attendance/backend - Features and flows doc:
/docs/developer/attendance/feature