StudyLyon — multi-tenant ERP / School Management API. This package designs the
Students module client (Flutter, forward-looking spec) against the implemented
NestJS backend. Every endpoint, DTO field, schema, enum, event, and wire contract is
derived from src/modules/students/**, src/modules/parents/**,
src/modules/academics/**, src/modules/bulk/**, src/modules/users/**,
src/modules/rbac/permissions.constants.ts, src/infrastructure/**, and
studylyon-blueprint/04-Modules/Students.md. Nothing is invented; gaps are flagged
(planned), (forward-looking), (proposed) or listed in Assumptions & Open
Questions.
Students is the academic heart of a school: it holds every enrolled child's academic
profile, their enrollment history per class and academic year, transfers and
promotions, graduation/withdrawal, archived records, uploaded documents, and the
links to parents/guardians that power family portals. It is the anchor record that
attendance, fees, results, library, transport and homework modules hang off.
Responsibility
Source
Profile creation with automatic first enrollment
student.service.ts:55-93
Enrollment into a class for an academic year (history kept, never overwritten)
Status values are fixed by StudentStatus (student.schema.ts:7-13):
active | inactive | graduated | transferred | archived.
┌────────────────────────────────────────────┐
│ POST /students (create) │
│ status := active (always) │
│ auto-enroll: ACTIVE enrollment created │
└────────────────────────────────────────────┘
│
┌────────────────────────┼───────────────────────────┐
▼ ▼ ▼
[active] ── transfer ──► enrollment TRANSFERRED + new ACTIVE ──► stays [active]
[active] ── graduate ──► status = graduated (409 if already graduated)
[active] ── archive ──► status = archived (409 if already archived)
[archived] ── restore ─► status = active (409 if already active)
any ── DELETE /students/:id ──► soft-delete (isDeleted=true, excluded from all queries)
Create forces status: ACTIVE and admissionDate = today when omitted
(student.service.ts:65-69), then immediately creates an ACTIVE
class_enrollments row for the given classId/academicYearId (student.service.ts:71-78).
Admission number is unique per tenant — duplicate → 409 (student.service.ts:56-62,
student.schema.ts:68).
Enroll deactivates all currently-active enrollments
(status: transferred, leftAt: now) and creates a fresh ACTIVE one
(student.service.ts:125-140). History is append-only — previous years are never
overwritten (blueprint 03-Database/COLLECTIONS.md:1674-1677).
Transfer is enroll + profile sync: requires current status active
(else 409), then updates classId, academicYearId, optional gradeId/sectionId
on the student doc (student.service.ts:170-203).
Graduate sets status graduated; idempotency guard → 409 when already
graduated (student.service.ts:205-223). Note: graduation does not close the
active enrollment — flagged in OQ-5.
Archive / restore toggles archived ↔ active with a same-state 409 guard
(student.service.ts:225-248).
inactive / transferred statuses exist in the enum but no service method
writes them — only legacy data or future use (OQ-6).
DELETE soft-deletes the student record only — documents, enrollments and
parent links are not cascaded (OQ-7, student.service.ts:157-168).
findById/enroll/transfer/update/remove/graduate/archive/restore/
uploadDocument on unknown id → 404 "Student not found."
(student.service.ts:95-99,124,143-145,159,174,206,226,255).
Transfer/graduate/archive on wrong status → 409 with the current status in the
message (student.service.ts:175-179,207-209,228-230).
Enroll while another enrollment is active → previous becomes transferred
(no error; expected workflow) (student.service.ts:125-131).
Pagination: sort and q query params are accepted but ignored by
StudentService.find (student.service.ts:104-108) — list search/sort is
(planned) server-side (OQ-8).
CSV malformed/empty → 400; unknown entity → 404; per-row failures reported,
never abort the batch (bulk-import.service.ts:31-63).
Duplicate email inside one CSV → UsersService.create fails → adapter falls
back to the existing user (re-link) (students-import.adapter.ts:66-75).
POST /students requires an existing userId (create-student.dto.ts:6-7) —
the UI must create the user (or reuse an existing one) before creating the
student. There is no "create user + student" composite endpoint.
Document upload has no server-side size/mime validation — only the
multipart field must exist; limits are client + proxy level today (OQ-9,
student.service.ts:250-271).
StudentCreated payload carries studentId, admissionNumber, classId
(student.service.ts:85-90); it routes to the in-app queue only — no email
(email worker handles only UserRegistered/PasswordResetRequested,
email.worker.ts:26-42). PLAN 4.1's "StudentCreated → ParentCreated → email"
chain is therefore (planned) (OQ-3).
RBAC: student.* permissions exist as constants (permissions.constants.ts:25-28)
but the students controller applies JwtAuthGuardonly (student.controller.ts:32-35);
RbacGuard is not wired on these routes — permission enforcement is
(planned) (OQ-4). The client should still gate UI by student.* per
00-shared/05 §9.
No parent.* permissions exist in permissions.constants.ts at all — the
parents surface has no permission vocabulary yet (OQ-10).
PRD: native mobile apps are Phase 3 (read-only companion) — this package is
the forward-looking full client spec (shared ledger 00-shared/12 A1).
(planned) (server): POST /students/bulk-import under /students
(docs/IMPLEMENTATION_PLAN.md:195 — today import lives at /bulk/import/students);
promote endpoint + StudentPromoted event (blueprint 04-Modules/Students.md:32,43);
student self-service (results/attendance read for the linked userId).
(forward-looking): profile photo upload at POST /users/:id/avatar
(users.controller.ts:95-102) exists server-side; QR admission cards, push of
StudentCreated to parents, WS live roster updates.
(proposed): analytics events (students.list.search, students.import.done…)
per 00-shared/10 §8.
The client treats active as the default roster filter; archived students are
hidden from lists unless "include archived" is toggled (server find() returns
everything not soft-deleted — filtering is client-side today, OQ-8).
Class dropdown data comes from GET /classes (+ by-year/:academicYearId),
grades from GET /grades, sections from GET /sections/by-grade/:gradeId,
academic years from GET /academic-years (academics/controllers/*.ts).
Identity edits (name/email/phone/avatar) are Users module screens; the
Students UI shows them read-only from the linked user.
Multipart uploads use field name file exactly (student.controller.ts:83,
bulk.controller.ts:38, users.controller.ts:96).
No self-service endpoints for students/parents (no "my profile", no GET /students/me). Parent view exists only via GET /parents/:id/students. When is student self-view added?
Student/Parent persona screens (03, 06)
OQ-2
StudentService.find accepts sort/q but ignores them — server-side search/sort/status filter planned?
List screen filter behaviour (05, 12, 13)
OQ-3
PLAN 4.1 chain "StudentCreated → ParentCreated → email" — no auto-parent-creation or student/parent email in code (email.worker.ts:26-42). Intended?
Journey "link parent" UX
OQ-4
student.* perms defined but RbacGuard not applied on students/parents/bulk controllers — enforcement when?
Permission gating in UI (04, 05)
OQ-5
graduate sets status but leaves the ACTIVE enrollment open — close it (leftAt) as part of graduation?
Academic-history rendering
OQ-6
inactive/transferred student statuses unwritable by any service method — legacy or future workflow?
Status chip legend
OQ-7
DELETE /students/:id soft-deletes the student only — documents/links/enrollments stay. Cascade or keep history?
Deletion UX copy
OQ-8
List has no status filter param — archiving hides nothing from GET /students. Client-side filtering or new query param?
Roster filtering
OQ-9
Document upload: no server size/type limits; student_documents has no downloadable-file endpoint (only fileId) — file retrieval via /api/v1/files/... local path (planned)
Documents tab + previews
OQ-10
No parent.* permission constants — how are parent CRUD routes authorized beyond JWT?