01 — Product Overview (Attendance Module)
- 1. Purpose
- 2. Business goals
- 3. User goals
- 4. Stakeholders
- 5. Why this exists
- 6. Scope boundaries (exact, from code)
- 7. Success metrics
- 8. Edge cases (backend-derived)
- 9. Assumptions (module)
- 10. Open questions (module; global ledger in 00-shared/12)
- 11. Glossary (this module)
StudyLyon — multi-tenant ERP / School Management API. This package designs the Attendance module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, DTO field, schema, index, event, worker, and permission is derived directly from
src/modules/attendance/**,src/modules/biometric/**,src/infrastructure/workers/attendance.worker.ts,src/infrastructure/bullmq/**,src/modules/scheduler/**,src/modules/reports/**, andsrc/modules/rbac/**. No feature is invented; gaps are flagged in Assumptions & Open Questions (§10) and marked(planned)/(forward-looking)/(proposed)inline.
1. Purpose
Attendance records one status per student per day — the daily truth a school runs on: who came, who didn't, who was late, who took leave. It is captured by teachers in the classroom (manual marking), ingested from biometric devices (raw punch logs), corrected after the fact (edit), and consumed everywhere: monthly reports, dashboards, absentee alerting, and parent/student visibility.
The domain is deliberately small: one attendance document = one student, one date,
one status (attendance.schema.ts:24-55), enforced by a unique index
(attendance.schema.ts:59-62).
| Responsibility | Source |
|---|---|
| Mark one student's attendance for a date (idempotent upsert) | attendance.service.ts:24-46; attendance.repository.ts:17-37 |
| Batch-mark many students at once | attendance.service.ts:48-54 (bulkMark) |
| Read class roster status for a date | attendance.service.ts:56-61 |
| Read a student's history (optional date range) | attendance.service.ts:63-73 |
| Read a single record / correct it (edit) | attendance.service.ts:75-79, 81-101 |
| Class summary over a date range (per-status counts) | attendance.service.ts:103-113 |
Emit AttendanceMarked / AttendanceUpdated domain events | attendance.service.ts:37-44, 88-99 |
Queue events → BullMQ attendance-process worker | event-queue-map.ts:14-21; attendance.worker.ts:16 |
| Absentee watch (≥3 absences → alert log) | attendance.worker.ts:69-80 |
| Biometric punch ingestion (raw logs) | biometric.controller.ts:14-18; biometric.service.ts:14-16 |
| Attendance summary report (async job) | reports.service.ts:110-137; report-job.schema.ts:8-9 |
| Daily attendance report schedule (07:00 UTC) | scheduler.service.ts:99-104; attendance-report.job.ts:13-24 |
| Dashboard attendance KPI (today, present %) | dashboard.service.ts:28-65 |
2. Business goals
| Goal | Measure |
|---|---|
| Classroom marking is fast | Single tap-cycle status per student; batch covers the whole class in one request (mark-attendance.dto.ts:51-54) |
| No double records | Unique index {tenantId, studentId, date} (attendance.schema.ts:59-62) + upsert findOrCreate (attendance.repository.ts:17-37) |
| Corrections are auditable | AttendanceUpdated event carries the changed field keys (attendance.service.ts:94-98); every doc has createdBy/updatedBy/version (base.schema.ts:13-31) |
| Absentee parents get alerted | AttendanceMarked → attendance-process queue → ≥3-absence alert path (attendance.worker.ts:69-80) — currently log-only, dispatch (planned) (OQ-1) |
| Reports without blocking the API | Async report-generate queue + report_jobs status polling (reports.controller.ts:14-24) |
| Multi-tenant isolation | Every query tenant-scoped by BaseRepository.scopedFilter (base.repository.ts:20-30); cross-tenant IDs → 404 |
3. User goals
- Teacher: mark a whole class in under a minute (bulk), fix a wrong status fast (PATCH), see today's marked/unmarked at a glance, never lose work on a slow connection.
- Org admin: monthly attendance rates per class, drill into per-student history,
ensure devices are pushing punches (
biometric-device.schema.ts:7-11). - Parent: get alerted the moment a child is marked absent, view the child's month at a glance.
- Student: see own attendance % and history (
END_TO_END_USER_FLOWS.md:324-325). - Biometric operator: ingest device punches, verify device health, understand what becomes attendance (and what currently doesn't — see OQ-3).
4. Stakeholders
Teachers (primary marker), class teachers (roster ownership), org admins (reports and policy), parents/students (consumers), biometric device operators (integration), scheduler (jobs), support staff (correction disputes), QA/design/engineering.
5. Why this exists
Attendance is the highest-frequency daily data-entry task in a school and the basis of report cards, fee policy, and parent trust. The backend makes the write path idempotent and cheap; the client's job is to make the capture surface so fast that teachers actually use it, and the consumption surfaces so clear that parents trust it.
6. Scope boundaries (exact, from code)
In scope today (backend): single mark, bulk mark (sequential loop over mark(),
attendance.service.ts:48-54), class-by-date read, student history read, summary
counts, PATCH edit, biometric log ingest (store-only), async attendance summary report,
dashboard KPI, scheduler cron registration, absentee watch (log-only).
Not implemented (flag labels used below):
- Session-based attendance (
sessionId,attendanceMode: 'session') —(planned), schema additions sketched indocs/IMPLEMENTATION_PLAN.md:432-468; the live schema has no session field (attendance.schema.ts). POST /attendance/importbulk import endpoint —(planned); blueprint04-Modules/Attendance.md:30lists it; only the worker'sbulk-importbranch exists (attendance.worker.ts:49,83-102), and it creates records directly with per-record error swallowing (duplicate keys fail silently — OQ-2).- Biometric logs → attendance derivation —
(planned)/(forward-looking);biometric-syncqueue +*/15 * * * *cron exist (scheduler.service.ts:71-76,queue.constants.ts:7) but no worker consumes it;biometric.service.ts:14-16only writes the log. BlueprintRELATIONSHIPS.md:100intendsbiometric_logs.processed → attendance. - Absentee notifications to parents —
(planned);attendance.worker.ts:69-80only logs "alert recommended". PLAN.md row 5.4 describes the target (AttendanceMarked→in-app+pushqueues). - Push/QR device flows —
(forward-looking)(shared ledger B3/B4: no device-token registry, no QR backend). - Client-side analytics —
(proposed)per 00-shared/10_QA_Baseline.md §8. - RBAC enforcement — permissions
attendance.mark/attendance.editare defined (permissions.constants.ts:29-30) and seeded (role.schema.ts:31), global guards areRateLimitGuard → JwtAuthGuard → RbacGuard(app.module.ts:129-131), but the attendance controller declares no@Permissions()decorator (attendance.controller.ts:19-23) — treat 403 as expected contract (OQ-5). - Pagination on attendance list endpoints — not implemented; reads return plain arrays
without
meta(OQ-4, see 12_API_Mapping.md).
7. Success metrics
- Teacher marks a 40-student class in < 60 s (bulk request ≤ 1 payload).
- 100% of mark attempts succeed or surface a clear error — the upsert means retries are
safe (idempotent,
attendance.repository.ts:23-29). - Zero duplicate attendance docs per tenant (unique index enforcement, plus client dedupe in offline queue).
- Correction latency: PATCH round-trip < 500 ms p95; conflict-free because PATCH replaces
only sent fields (
update-attendance.dto.ts:4-22+$set,base.repository.ts:57-66). - Attendance report generated async without request timeout (
reports.service.ts:28-44).
8. Edge cases (backend-derived)
- Remark same student+date → overwrite (upsert), source stays manual unless supplied;
classIdon an existing doc is not updated by remark (attendance.repository.ts:23-29). - Same student marked for a different class on the same date → still one doc (unique on
studentId+date), originalclassIdwins — client must prevent cross-class remarking. - Invalid status on POST → 400
VALIDATION_ERROR(IsEnum,mark-attendance.dto.ts:27-28; e2ep1-operations.e2e-spec.ts:222-228). - Invalid status on PATCH → not enum-validated (
update-attendance.dto.ts:8isIsStringonly) — server may persist a bad status via$set(no runValidators); client must constrain the picker (OQ-6). - Absentee count in the worker counts all absent records for the student, not
consecutive days within a window (
attendance.worker.ts:70-74) — UI must not promise "3 consecutive absences" semantics (OQ-1). - Duplicate records in
bulk-import→ per-record catch → logged warn, not failed (attendance.worker.ts:91-101). dateis sent asYYYY-MM-DDstring and stored asDate(mark-attendance.dto.ts:21;attendance.schema.ts:31-32) — timezone discipline is a client responsibility (see 14_QA_Checklist.md §timezone).- Deleted/cross-tenant record → 404 "Attendance record not found." (
attendance.service.ts:77). - Summary with no records →
{total: 0, summary: {}}(attendance.service.ts:112).
9. Assumptions (module)
- Client is forward-looking (see shared ledger A1 — PRD Phase 1 is backend; these docs specify the full Flutter client per user instruction).
- One attendance per student per day is the only current model; "per session" is
(planned)and the client's calendar/history must not assume per-period rows. - Teacher role carries
['student.read','attendance.mark','attendance.edit'](role.schema.ts:31); admin has everything (role.schema.ts:23). Parent and student roles have no attendance permission today (role.schema.ts:55,63) — the student profile and parent views must be built againststudent.read+ the routes those roles reach (see 12_API_Mapping.md §9, OQ-7). - Enrollment is the roster source:
Student.classId/sectionId(student.schema.ts:38-39) andclass_enrollments(class-enrollment.schema.ts:14-38); the marking grid renders students of one class. - Retry safety: because mark is an upsert, a re-sent offline batch is harmless
(repeated sends converge), but events fire per mark — network-layer dedupe is still
recommended to avoid duplicate
AttendanceMarkedevents.
10. Open questions (module; global ledger in 00-shared/12)
| # | Item | Impact |
|---|---|---|
| OQ-1 | Absentee worker counts all absences (not consecutive, no window) and only logs "alert recommended" — no notification is dispatched. When does the real parent alert land, and what are its semantics? | Alert copy, "consecutive vs total" messaging |
| OQ-2 | bulk-import worker swallows per-record failures (duplicate keys). Should failures surface in a result report for the importing admin? | Import UX, partial-success UI |
| OQ-3 | biometric-sync queue + 15-min cron exist but no worker consumes them; logs never become attendance docs. When is the punch→attendance derivation built? | Device status screen promises |
| OQ-4 | Attendance reads return unpaginated arrays (a 60-student day is fine, but a term of history is not). Is pagination added? | History screen strategy (client-side paging fallback) |
| OQ-5 | No per-endpoint RBAC decorators on attendance; global RbacGuard present (app.module.ts:131) but no permission metadata on the controller. Confirm the server contract for 403. | Permission-gated UI visibility |
| OQ-6 | PATCH status is a free string (no IsEnum) and findOneAndUpdate runs without runValidators — a bad status may persist. Server fix or client-only constraint? | Status picker validation strategy |
| OQ-7 | Parent/student roles have no attendance permissions; yet user flows (END_TO_END_USER_FLOWS.md:324-325,378-379) show students/parents reading attendance. Which permission should gate read-only history? | Parent/student screen gating |
| OQ-8 | getSummary and reports aggregate in memory (no aggregation pipeline) — a full term × all students could be slow. Watch for latency at scale (INDEXING.md:67,93 notes sharding candidacy). | Report UX, loading states |
11. Glossary (this module)
| Term | Meaning |
|---|---|
| Attendance doc | One row: studentId + classId + date + status (+ checkIn/checkOut/source/remarks) |
| Status | present, absent, late, half_day, leave, holiday (attendance.schema.ts:7-14) |
| Source | manual, biometric, import, api (attendance.schema.ts:16-21) |
| Mark | POST create/upsert of one day's record |
| Remark / Overwrite | Same student+date POST again → fields overwritten in place |
| Bulk | POST /attendance/bulk — sequential marks for many records (attendance.service.ts:48-54) |
| Punch | Raw biometric_logs row (student, device, timestamp) — not yet attendance |
| Roster | Students of a class (via student.classId) rendered for marking |
| Summary | {total, summary:{status:count}} from GET /attendance/summary |
| Attendance report | Async attendance_summary report job (report-job.schema.ts:9) |
| Envelope | {success,message,data,meta?,timestamp,requestId} (shared 00-shared/07) |