01 — Product Overview (Academics Module)
- 1. Purpose
- 2. Business goals
- 3. User goals
- 4. Stakeholders
- 5. Why this exists
- 6. Dependencies
- 7. Success metrics
- 8. Edge cases
- 9. Assumptions (module)
- 10. Open questions (module-level; global ledger in
00-shared/12) - 11. Glossary (this module)
StudyLyon — multi-tenant ERP / School Management API. This package designs the Academics module client (Flutter, forward-looking spec) against the implemented NestJS backend. All endpoints, DTO fields, schemas, indexes, business rules, and wire contracts are derived directly from
src/modules/academics/**,src/database/base.*,src/common/dto/pagination-query.dto.ts,studylyon-blueprint/03-Database/*,04-Modules/*,PLAN.md, anddocs/IMPLEMENTATION_PLAN.md. No feature is invented; gaps are flagged in the Assumptions & Open Questions section.
1. Purpose
Academics is the structural backbone of StudyLyon. It defines the five reference entities every operational module hangs off:
| Entity | Collection | Meaning | Source |
|---|---|---|---|
| Academic Year | academic_years | A session (e.g. "2026–2027") with dates, status, isCurrent flag | schemas/academic-year.schema.ts:13 |
| Grade | grades | A year level ("Grade 1", "Grade 10"), optional per-year, displayOrder | schemas/grade.schema.ts:7 |
| Section | sections | A division of a grade ("A", "B", "C") | schemas/section.schema.ts:7 |
| Class | classes | The concrete teaching unit: year + grade + section (e.g. "Grade 10 - A") | schemas/class.schema.ts:7 |
| Subject | subjects | Master catalog (code, marks configuration, credits) | schemas/subject.schema.ts:7 |
| Subject Assignment | subject_assignments | Teacher ↔ Subject ↔ Class ↔ Year binding | schemas/subject-assignment.schema.ts:7 |
1.1 Why it is the backbone
Downstream modules reference these entities by ObjectId and are meaningless without them (verified in source):
- Timetable requires
classId,subjectId,teacherId,academicYearId(timetable/schemas/timetable.schema.ts:18-40). - Attendance requires
classId(attendance/schemas/attendance.schema.ts:28-29). - Students enrollment binds to
classIdper year (blueprint04-Modules/Students.md:31). - Homework / Exams / Results / Fees consume classes and subjects as reference data
(blueprint
04-Modules/Exams.md:51,03-Database/RELATIONSHIPS.md §4, §6).
The canonical hierarchy (blueprint 03-Database/RELATIONSHIPS.md:50-61):
organizations
└─ academic_years (many, per tenant)
└─ grades (many)
└─ sections (many)
└─ classes (many) via gradeId + sectionId
├─ students (many via class_enrollments)
├─ timetables (many)
└─ homework (many)
subject_assignments binds the teaching staff into this tree
(03-Database/RELATIONSHIPS.md:84-92):
teachers (1)
└─ subject_assignments (many) via teacherId
├─ subjects (via subjectId)
└─ classes (via classId)
2. Business goals
| Goal | Measure | Evidence |
|---|---|---|
| One-click year rollover | PATCH /academic-years/:id/set-current toggles exactly one isCurrent per tenant (services/academic-year.service.ts:69-83) | blueprint rule: "Only one academic year can be active per organization" (COLLECTIONS.md:1023) |
| Zero name collisions on master data | unique-per-tenant checks: academic-year name (academic-year.service.ts:25-29), grade name (grade.service.ts:21-23), subject code + DB unique index (subject.service.ts:21-25, subject.schema.ts:38) | 409 DUPLICATE_RESOURCE |
| Ordered, predictable grade ladder | displayOrder sort default on grades (grade.service.ts:39); index {tenantId, displayOrder} (grade.schema.ts:27) | |
| Full multi-tenant isolation | every query tenant-scoped by BaseRepository.scopedFilter() (database/base.repository.ts:20-30) | |
| Downstream consistency | timetable/attendance filter by classId/academicYearId; changes here propagate to their lists (timetable.service.ts, attendance.schema.ts:63) |
3. User goals
- Organization admin: set up the year, grade ladder, sections, subjects once; flip the current year at rollover; see the whole structure at a glance.
- Academic coordinator: build grades → sections → classes, assign subjects and teachers per class per year, catch conflicts before timetabling starts.
- Teacher: see which classes/subjects they teach in the current year; use class lists to find their sections.
- Student / parent (read-only): browse the current year's structure — grade, section, class name, subject list — to understand where they sit.
4. Stakeholders
Platform operator, institution admins, academic coordinators, teachers, students, parents, timetable/attendance/homework/exam feature owners (consumers of this reference data), QA + design + engineering.
5. Why this exists
Every school runs on a fixed academic calendar and ladder. StudyLyon is multi-tenant
with heterogeneous school structures (per blueprint COLLECTIONS.md:925-969); a
tenant-defined year/grade/section/class tree is the precondition for enrollment,
attendance, timetable, exams, and fees. Without this module those modules cannot
target a student's actual teaching unit.
6. Dependencies
| Dependency | Role | Source |
|---|---|---|
| Auth (JWT) | every endpoint guarded by JwtAuthGuard | academics.module.ts:4,46; all controllers @UseGuards(JwtAuthGuard) |
| RBAC permissions | gap: no academics.* permission exists in ALL_PERMISSIONS — only JWT presence is enforced today | rbac/permissions.constants.ts:1-97 (see OQ-1) |
TenantContextService | tenant scoping + platform-admin bypass | repositories/*.ts, database/base.repository.ts:20-30 |
| Mongo indexes | {tenantId, code} unique on subjects; per-entity tenant+ref indexes | subject.schema.ts:38, class.schema.ts:38-40 |
| Timetable / Attendance / Students / Homework / Exams | consumers of class/subject/year references | timetable/schemas/timetable.schema.ts:18-40, attendance/schemas/attendance.schema.ts:28-29 |
7. Success metrics
- Year setup (year → grade → section → class → subject → assignment) completed in < 15 min for a 12-grade school on a mid-range device.
- Zero duplicate master records per tenant (server 409s surfaced correctly, never silently overwritten).
- Hierarchy browse renders 12 grades × 4 sections × 40 classes with no jank
(long-list budgets per
00-shared/10 §1). - Conflict surface (duplicate class name, teacher double-booked) visible before
timetable creation — flagged
(planned)since the API has no conflict endpoint today (OQ-4).
8. Edge cases
- Duplicate academic-year name → 409 "Academic year "X" already exists."
(
academic-year.service.ts:27-28); duplicate grade name → 409 (grade.service.ts:23); duplicate subject code → 409 + unique index fallback (subject.service.ts:23-24,subject.schema.ts:38). - Classes/sections/assignments have no duplicate guard in code — duplicate
(gradeId, sectionId, name)classes and duplicate assignments are possible (OQ-4):class.service.ts:16-18,subject-assignment.service.ts:13-17. - Soft delete everywhere (
BaseRepository.softDelete,base.schema.ts:20-27) — lists hide deleted docs automatically; no cascade — deleting a grade leaves orphan sections/classes behind (OQ-3,RELATIONSHIPS.md:134-138). - Grade/section/class
statusis a free string (schema default'active'), not an enum — client must treat any non-'active'value as inactive (grade.schema.ts:21-22,section.schema.ts:24-25,class.schema.ts:33-34). set-currentun-flags everyisCurrentthen flags the target and setsstatus: active(academic-year.service.ts:72-79); the target may have hadstatus: upcomingorarchived.- Grade
academicYearIdis optional in both schema and DTO — grades can exist outside any year (grade.schema.ts:9-10,create-grade.dto.ts:5-8); classacademicYearIdis required (class.schema.ts:9-10). - Subject marks are independent ints — no cross-field validation
(theory+practical vs maximum, passing ≤ maximum) in DTO or service
(
create-subject.dto.ts:18-46, OQ-5). UpdateSubjectDtocannot changetheoryMarks/practicalMarks(update-subject.dto.ts:4-42).- Assignments have no
GET /subject-assignments/:idand no update endpoint — remove + recreate is the only correction path (subject-assignment.controller.ts:19-39). class.campusIdhasref: 'Class'in the schema (likely intendedCampus) (class.schema.ts:12-13) androomIdis a free string, not aRoomref (class.schema.ts:30-31) — client renders these as opaque IDs/strings (OQ-6).- Academic years list default sort
createdAt: -1(newest first) (academic-year.service.ts:47); grades defaultdisplayOrder: 1(grade.service.ts:39); classes/sections/subjects/assignments are unsorted (insertion order) (class.service.ts:44-48,section.service.ts:41-48,subject.service.ts:35-47).
9. Assumptions (module)
- Mobile client is forward-looking: the backend is complete; this package is the UI-side spec against real endpoints.
- PRD / roadmap context: native mobile apps are out of Phase 1 scope
(
PRODUCT_REQUIREMENTS_DOCUMENT.md:144); roadmap Phase 3 = read-only companion — these docs still spec the full client per global decision A1 (00-shared/12 A1). Academics admin/coordinator editing is speced for tablet/desktop-first; phone gets read + quick actions. - The coaching-extension plan (
docs/IMPLEMENTATION_PLAN.md §6.2.1: sections → batches,subjectCategory,classType,batchIdfields) is not in code; all of it is marked(planned)and never shown as implemented. - The wire contract follows the fixed envelope; academics endpoints return raw
Mongoose documents inside
data(no field whitelisting/renaming at the API layer) — the client maps_id,__v, timestamps explicitly (base.schema.ts:7-35). - No pagination helper exists on assignments/
by-class/by-teacherlookups — they return bare arrays (subject-assignment.service.ts:19-31).
10. Open questions (module-level; global ledger in 00-shared/12)
| # | Item | Impact |
|---|---|---|
| OQ-1 | No academics.* permissions in ALL_PERMISSIONS (permissions.constants.ts:1-97) and no RbacGuard on any academics controller — any authenticated user (student/parent) can create/update/delete structure. When are perms + guard added? | Screen gating, hidden CTAs, client permission map |
| OQ-2 | No tenant-level unique constraint on academic-year name / grade name (service check only, race-prone); subjects have a DB unique index. Add compound unique indexes? | 409 UX, concurrency |
| OQ-3 | Deleting a grade/section/class is a bare soft-delete with no cascade and no integrity check — orphan sections/classes/assignments/attendance refs are possible. Backend cascade job (planned)? | Delete confirm copy, orphan handling |
| OQ-4 | No duplicate-class / duplicate-assignment validation; no conflict endpoint for teacher double-booking or class-name reuse per year. Front-end only, or backend validation? | Conflict UI, error copy |
| OQ-5 | Subject marks fields have no cross-field validation (theory+practical ≤ maximum, passing ≤ maximum); update cannot edit theory/practical marks. Backend fix planned? | Form validation messaging |
| OQ-6 | class.campusId ref points at Class (typo for Campus?) and roomId is a free string — no campus/room modules exist yet in code. Resolve before linking campus/room pickers. | Campus/room pickers (planned) |
| OQ-7 | PaginationQueryDto.q (global search) is parsed but never used by any academics service (services filter {} only) — server-side q search (planned). | Client search UX |
11. Glossary (this module)
| Term | Meaning |
|---|---|
| Academic Year | academic_years doc; status ∈ `upcoming |
| Grade | grades doc; level name + optional code + displayOrder; optional per-year binding |
| Section | sections doc; division of a grade (name A, B, …), own capacity/teacher/room |
| Class | classes doc; the teaching unit year + grade + section + name; required refs to year/grade/section |
| Subject | subjects doc; catalog entry with code (unique per tenant) and marks config (max/passing/theory/practical) |
| Subject Assignment | subject_assignments doc; teacher×subject×class×year tuple |
set-current | PATCH /academic-years/:id/set-current — promotes one year; demotes all others |
| Envelope | {success,message,data,meta?,timestamp,requestId} (00-shared/07 §2) |