01 — Product Overview (Teachers Module)
- 1. Purpose
- 2. Module boundary: Teachers vs Staff
- 3. Teacher lifecycle (end-to-end)
- 4. Subject & class assignment — two mechanisms (important)
- 5. Permissions reality (read carefully)
- 6. Events & side-effects
- 7. Dependencies
- 8. Business goals
- 9. Success metrics
- 10. Edge cases (derived)
- 11. Assumptions (module)
- 12. Open questions (module-level)
StudyLyon — multi-tenant ERP / School Management API. This package designs the Teachers module client (Flutter, forward-looking spec) against the implemented NestJS backend. All endpoints, DTO fields, schemas, events, and business rules are derived directly from
src/modules/teachers/**,src/modules/academics/**(subject assignments),src/modules/staff/**,src/modules/rbac/**, andsrc/infrastructure/**. No feature is invented; gaps are flagged in Assumptions & Open Questions and marked(planned)/(proposed).
1. Purpose
Teachers is the teaching-profile domain. It answers "who teaches here" for an
institution: the employment record of every instructor (linked to a User identity),
their department/designation, employment status, qualifications, the subjects they
teach and the classes they are class-teacher for, plus — via the
subject_assignments collection owned by the Academics module — the per-academic-year
matrix of which teacher teaches which subject in which class.
| Responsibility | Source |
|---|---|
| Teacher profile CRUD (create/read/update/deactivate) | teacher.controller.ts:24-38 |
| Duplicate guards (userId, employeeNumber) on create | teacher.service.ts:27-38 |
Soft-delete (deactivate) with isDeleted flag | base.repository.ts:68-74, teacher.service.ts:95-106 |
Employment status enum (active/inactive/on_leave/terminated) | teacher.schema.ts:7-12 |
| Subject ↔ class ↔ academic-year teaching matrix | subject-assignment.controller.ts:21-38, subject-assignment.schema.ts:9-19 |
| Denormalized subject/class-teacher references on profile | teacher.schema.ts:44-48 |
| Domain events → in-app notification + audit + search index | teacher.service.ts:40-50,84-91,98-105; event-queue-map.ts:31-33; search-indexer.service.ts:10,17,24 |
| Teacher count KPI for dashboard | dashboard.service.ts:32,52 |
2. Module boundary: Teachers vs Staff
The backend splits "people" into two parallel profile documents that share the same shape for employment basics (userId, employeeNumber, departmentId, designationId, joiningDate) but diverge on domain specifics:
| Aspect | Teacher (teachers) | Staff (staff) |
|---|---|---|
| Blueprint identity | "Teaching profile." (COLLECTIONS.md:1418-1422) | "Non-teaching employees." (blueprint 04-Modules/Staff.md:3) |
| Employment fields | employmentStatus, qualification, experienceYears (teacher.schema.ts:28-42) | employmentType, salaryGrade (staff.schema.ts:35-47) |
| Academic linkage | subjects[], classTeacherFor[] (teacher.schema.ts:44-48) | none |
| Unique constraints | tenantId+userId, tenantId+employeeNumber (teacher.schema.ts:56-57) | tenantId+userId, tenantId+employeeNumber (staff.schema.ts:57-58) |
| CRUD surface | teacher.controller.ts:24-38 | staff.controller.ts:24-38 |
| Events | TeacherCreated/Updated/Deleted | StaffCreated/Updated/Deleted |
| Reference catalogs | departments/designations owned by Staff module (Staff.md:59) | departments/designations owned here |
| Status field name | employmentStatus (enum EmploymentStatus) | status (enum StaffStatus) — same 4 values |
A single users record may own a teacher profile and a staff profile (e.g. a teacher
who also works the front office): RELATIONSHIPS.md:32-33 ("A single users record may
own multiple profiles"). Both can even coexist with the same userId because they are
separate collections with separate uniqueness scopes. The client must treat them as
two different records — the Teachers screen never renders Staff records, and vice
versa.
3. Teacher lifecycle (end-to-end)
User account created (Users module)
→ Teacher profile created POST /teachers [TeacherCreated → in-app]
→ Subjects + classes assigned (two mechanisms, §4)
→ Day-to-day teaching (timetable, attendance, homework — other modules)
→ Profile edited PATCH /teachers/:id [TeacherUpdated → audit]
→ Deactivated DELETE /teachers/:id [TeacherDeleted → audit]
(soft-delete: isDeleted=true, queries exclude; PLAN.md:146)
Deactivation is permanent from the client's perspective — there is no
re-activation endpoint. DELETE sets isDeleted: true + deletedAt + deletedBy
(base.repository.ts:68-74); every subsequent query excludes the record
(base.repository.ts:20-30).
4. Subject & class assignment — two mechanisms (important)
The backend has two parallel, non-synchronized ways to link a teacher to subjects/classes:
- Profile arrays —
teacher.subjects[](refSubject) andteacher.classTeacherFor[](refClass), set viaPOST /teachersorPATCH /teachers/:id(create-teacher.dto.ts:44-52,teacher.schema.ts:44-48). These are denormalized profile data — there is no server logic that reads them for scheduling. subject_assignmentscollection — one document per (teacher, subject, class, academicYear) triple:subject-assignment.schema.ts:9-19, managed byPOST /subject-assignments,GET /subject-assignments/by-class/:classId,GET /subject-assignments/by-teacher/:teacherId,DELETE /subject-assignments/:id(subject-assignment.controller.ts:21-38). This is the authoritative teaching matrix per academic year (RELATIONSHIPS.md:82-92).
Additionally classes.classTeacherId exists (class.schema.ts:28) as a third
class-teacher reference. The three stores are not kept in sync by any code — the
client must decide which is canonical for which surface (see OQ-4).
5. Permissions reality (read carefully)
permissions.constants.tscontains noteacher.*permissions — the teachers endpoints carry none ofstaff.*,department.manage,designation.manageeither (permissions.constants.ts:19-24).TeacherControlleris guarded only by@UseGuards(JwtAuthGuard)(teacher.controller.ts:18-21). The globalRbacGuard(app.module.ts:131) passes any request that carries no@Roles/@Permissionsmetadata (rbac.guard.ts:29).- The default
teacherrole ships with['student.read','attendance.mark','attendance.edit'](role.schema.ts:26-32) — nothing that lets a teacher read their own profile viaGET /teachers/:idis gated server-side; the client must enforce "teacher sees own record only" in UI/routing (OQ-1). docs/user-flows/END_TO_END_USER_FLOWS.md:751anticipatesteacher.*permissions ("✅ Admin, Read own — Teacher") — this is a doc-only contract, not implemented.
6. Events & side-effects
| Event | Emitted at | Queue route | Side-effect |
|---|---|---|---|
TeacherCreated | teacher.service.ts:40-50 (payload {teacherId, employeeNumber}) | in-app / job teacher-created (event-queue-map.ts:31) | In-app notification (inapp.worker.ts:46-53); search index Teacher (search-indexer.service.ts:10) |
TeacherUpdated | teacher.service.ts:84-91 (payload {teacherId}) | audit-write / log-teacher-updated (event-queue-map.ts:32) | Audit log; search re-index |
TeacherDeleted | teacher.service.ts:98-105 (payload {teacherId}) | audit-write / log-teacher-deleted (event-queue-map.ts:33) | Audit log; search index removal (search-indexer.service.ts:52-59) |
Two derivable gaps (see 14-QA): PLAN.md:36 promises "→ email queue → welcome email"
but the actual mapping sends TeacherCreated to the in-app queue, not emails;
and inapp.worker.ts:47 stores type: 'TeacherCreated' into a notification whose
NotificationType enum only admits email_verified|password_reset|verification_resent|welcome
(notification.schema.ts:7-12) — an enum-mismatch risk.
7. Dependencies
| Dependency | Role | Source |
|---|---|---|
| Users module | identity link (userId → firstName/lastName/email) | user.schema.ts:17-29; RELATIONSHIPS.md:22-30 |
| Staff module (Academics-owned) | departments, designations catalogs | staff.controller.ts (departments/designations controllers), department.schema.ts:9-19 |
| Academics module | subjects, classes, academic_years, subject_assignments | subject.schema.ts:9-34, class.schema.ts:9-34, academic-year.schema.ts:14-32 |
| Academics: class teacher | classes.classTeacherId | class.schema.ts:28 |
| Timetable module | teacher schedule (GET /timetable?teacherId=) | timetable.controller.ts:21-27, timetable.service.ts:55-60 |
| Dashboard module | teacher count KPI | dashboard.service.ts:32,52 |
| Leave module | substitute-teacher flows reference teacherRepo | leave.service.ts:232-277 |
BullMQ in-app / audit-write | event side-effects | queue.constants.ts:5,11, event-queue-map.ts:31-33 |
| Mongo collections | teachers, subject_assignments, subjects, classes, academic_years, departments, designations | COLLECTIONS.md:1418,1644 |
8. Business goals
| Goal | Measure |
|---|---|
| Create a teacher in < 1 min of form time | 2 server calls max (user exists → create profile) |
| No duplicate teachers | 409 on duplicate userId / employeeNumber (teacher.service.ts:27-38) |
| Assignment matrix accurate per academic year | subject_assignments scoped by academicYearId in every query (subject-assignment.service.ts:19-31) |
| Deactivation is safe & reversible by ops | soft-delete only; nothing is physically removed |
| Tenant isolation structural | every repo call injects tenantId + isDeleted:false (base.repository.ts:20-30) |
9. Success metrics
- Teacher creation → visible in list + dashboard count ≤ 3 s after submit.
- 0 duplicate employeeNumber errors for > 99% of form flows (client validates against existing list + server 409 handles races).
- Assignment editor: no silent conflicts — server has no conflict check on
POST /subject-assignments(subject-assignment.service.ts:13-17) so the client must surface duplicates itself (OQ-2). - Deactivate flow: confirmation dialog → success in 1 call, teacher disappears from all lists (soft-delete) but audit/search retention honored.
10. Edge cases (derived)
- Duplicate
userId→ 409 "Teacher profile already exists for this user." (teacher.service.ts:29-31). - Duplicate
employeeNumber→ 409 "Employee number "X" already exists." (teacher.service.ts:35-38). GET /teachers/:idunknown id → 404 "Teacher not found." (teacher.service.ts:56).DELETEunknown id → 404 (softDelete returns false → throw) (teacher.service.ts:96-97).PATCHunknown id → 404 viafindByIdpre-check (teacher.service.ts:81).GET /subject-assignments/by-teacher/:teacherIdwithoutacademicYearId→ filter is{teacherId, academicYearId: undefined}→ empty result (query param is required in practice though not validated) (subject-assignment.service.ts:26-31).DELETE /subject-assignments/:idunknown id → 404 "Assignment not found." (subject-assignment.service.ts:34-35).PATCH /teachers/:idcannot changeuserId(not inUpdateTeacherDto) — identity link is immutable via this API.sortandqparams are accepted byPaginationQueryDtobut not applied inTeacherService.find(teacher.service.ts:66-78) — client-side search/sort needed or backend enhancement (OQ-3).
11. Assumptions (module)
- Client is forward-looking: backend is complete for this module; these docs are the
UI-side spec, consistent with shared ledger A1 (
00-shared/12) — PRD lists native mobile as a later-phase, read-only companion; this package specs a full client per user instruction. subjects[]/classTeacherFor[]arrays andsubject_assignmentsare treated by the client as complementary: profile arrays render as badges/profile data; the assignments matrix drives the per-year schedule/assignment surfaces (OQ-4 for sync).- No bulk-import surface exists for teachers (
BulkModuleexists atapp.module.ts:59but teacher import is not implemented) → bulk import is(planned), not specced as v1. - Teacher photo/avatar: no photo field exists on
TeacherorUserin scope — avatar rendering uses initials fallback (AppAvatar,03_Component_Library.md).
12. Open questions (module-level)
| # | Item | Impact |
|---|---|---|
| OQ-1 | No teacher.* permissions exist and no endpoint carries RBAC metadata — who may view/edit which teacher? Is a self-service "my profile" endpoint (GET /teachers/me) planned? | Screen access rules, permission UI |
| OQ-2 | POST /subject-assignments has no duplicate/conflict guard (subject-assignment.service.ts:13-17) — duplicate (teacher,subject,class,year) is storable. Client-side dedup only, or backend unique index? | Assignment editor UX |
| OQ-3 | sort/q ignored by TeacherService.find (teacher.service.ts:66-78) — implement client-side filter/sort or extend backend? | List screen |
| OQ-4 | Three class-teacher sources (teacher.classTeacherFor[], classes.classTeacherId, none in assignments) — which is canonical for "class teacher" display? | Detail tabs |
| OQ-5 | Deactivation doesn't check active assignments/timetable/substitutions — deactivating a teacher with a live schedule is allowed. Guard added where? | Deactivate dialog copy |
| OQ-6 | TeacherCreated in-app notification writes a type outside the NotificationType enum (notification.schema.ts:7-12 vs inapp.worker.ts:47) — likely DB validation failure; needs backend fix before notifications render. | Notification UX |