01 — Product Overview (Exams 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 Exams module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, DTO field, schema, index, event, and permission is derived directly from
src/modules/exams/**,src/modules/results/**,src/infrastructure/bullmq/event-queue-map.ts,src/modules/rbac/permissions.constants.ts, anddocs/IMPLEMENTATION_PLAN.md. No feature is invented; gaps are flagged in Open Questions (§10) and marked(planned)/(forward-looking)/(proposed)inline.
1. Purpose
Exams is the academic evaluation backbone: exam planning (schedule per academic
year, type, status), subject scheduling (which class sits which subject on which
date/time), marks entry per (exam, student, subject), and publication that
releases results to the Results domain. The module owns two collections
(examinations, examination_subjects); marks live in the Results module's
examination_results collection (examination-result.schema.ts:7-8) — the Exams
service writes them directly (examination.service.ts:11, 176-182).
| Responsibility | Source |
|---|---|
Create an exam (always starts status: 'draft') | examination.service.ts:43-59 |
| List exams (paginated) / get one / update / soft-delete | examination.service.ts:61-110 |
| Add a subject-slot to an exam (class + date + time + marks) | examination.service.ts:112-131; examination-subject.schema.ts:9-31 |
| List an exam's subject-slots | examination.service.ts:133-137 |
| Enter/upsert marks for one student on one subject | examination.service.ts:139-195 |
| Publish an exam's results (atomic-ish, two writes) | examination.service.ts:203-220 |
| Read results by student / by exam-subject / report card | result.controller.ts:16-37; result.service.ts:36-98 |
| Grade letter computed from overall percentage | result.service.ts:130-138 |
Emit domain events (ExaminationCreated, MarksEntered, ExamResultsPublished, …) | examination.service.ts:50-57, 163-174, 212-219 |
ExamResultsPublished → BullMQ in-app queue, job results-published | event-queue-map.ts:27 |
2. Business goals
| Goal | Measure |
|---|---|
| Exam schedule creation per class/term is fast | One POST per exam (POST /examinations), one POST per subject slot (POST /examinations/:id/subjects) |
| Marks entry is per (exam, student, subject) and idempotent | Upsert by (tenantId, studentId, examinationSubjectId) unique index (examination-result.schema.ts:31-33); re-entry updates in place (examination.service.ts:147-175) |
| Marks never exceed the subject maximum | Server guard throws NotFoundException('Marks cannot exceed maximum.') when marksObtained > maximumMarks (examination.service.ts:145-146) — note: it surfaces as 404, client must pre-validate |
| Publication releases results atomically | publishResults stamps publishedAt on all results of the exam, then flips exam status → 'published' (examination.service.ts:203-220) |
| Multi-tenant isolation | Every query tenant-scoped by BaseRepository.scopedFilter (base.repository.ts:20-30); cross-tenant IDs → 404 |
| Auditability | All docs carry createdBy/updatedBy/version (base.schema.ts:13-31); $inc version on every update (base.repository.ts:57-66); domain events per mutation |
3. User goals
- Exam coordinator (admin/principal): create the term's exam calendar, attach
subject-slots per class, watch statuses
draft → active → completed → published, publish at the right moment. - Teacher: find my class's subject slot, enter marks for a whole class quickly, correct a wrong mark, never exceed the subject maximum.
- Student: see my marks per subject, open my report card (aggregate with percentage + overall grade).
- Parent: view the child's report card once published.
- Principal/analyst: see exam coverage (which subjects have/need marks) and the publish state of every exam.
4. Stakeholders
Exam coordinators (primary planners), teachers (marks entry), students & parents (consumers of published results), principal (publish authority), support staff (correction disputes), QA/design/engineering.
5. Why this exists
Exams are the highest-stakes academic workflow: the schedule drives the whole school calendar, marks become report cards, and a premature publish erodes trust. The backend provides a simple, idempotent CRUD + publish surface; the client must make planning and marks entry so efficient that teachers finish a class's marks in one sitting — and make publish an explicit, confirmable act.
6. Scope boundaries (exact, from code)
In scope today (backend):
POST/GET /examinations,GET/PATCH/DELETE /examinations/:id(examination.controller.ts:27-44)POST/GET /examinations/:id/subjects(examination.controller.ts:45-53)POST /examinations/:id/publish(examination.controller.ts:54-56)GET /results/student/:studentId,GET /results/exam-subject/:examSubjectId,POST /results/exam-subject/:examSubjectId/marks,GET /results/report-card/:studentId/:examId(result.controller.ts:16-37)- Domain events:
ExaminationCreated,ExaminationUpdated,ExaminationDeleted,ExaminationSubjectAdded,MarksEntered,ExamResultsPublished(examination.service.ts:50-57, 88-96, 102-110, 119-130, 163-174, 212-219)
Not implemented (flag labels used below):
POST/GET /examinations/:id/schedule,/hall-tickets,/seating-plan,/marks-import,POST/GET/PATCH /examinations/:id/re-evaluation—(planned);docs/IMPLEMENTATION_PLAN.md:213-220(Phase 4, "Missing Endpoints").- Mock tests, DPP, test series —
(planned); examtypeenum extension withmock_test | dpp | practice_test | all_india_test,isAllIndiaRank,testSeriesId, plustest_series/dppcollections andExamsService.createMockTest/createDPP/createTestSeriessketched indocs/IMPLEMENTATION_PLAN.md:501-567, 643. - RBAC enforcement — no
exam.*permission exists in code (permissions.constants.ts:1-97lists noexam.*; blueprintstudylyon-blueprint/04-Modules/Exams.md:64-72specifiesexam.create/update/ delete/mark/publishbut they are not seeded) andExaminationControllerdeclares no@Permissions()decorator (examination.controller.ts:21-24) — treat 403 as contract once RBAC lands (OQ-4). - Conflict detection with Timetable (
studylyon-blueprint/04-Modules/Exams.md:60) — no timetable check exists inexamination.service.tstoday (OQ-2). - Notifications for schedule/result alerts —
(planned); only thein-appresults-publishedjob is routed (event-queue-map.ts:27); other exam events have no queue route (event-queue-map.tscontains no entry forExaminationCreatedetc.). - Client-side analytics —
(proposed)per 00-shared/10_QA_Baseline.md §8.
7. Success metrics
- Coordinator creates an exam + 5 subject slots in < 3 min (6 requests).
- Teacher enters a 40-student class's marks in < 5 min — one POST per row, upsert safe for retries.
- 0 marks persisted above a subject maximum (server guard + client pre-validation).
- Publish round-trip < 1 s; post-publish read of results shows
publishedAtstamped. - Report card always renders a full subject list — missing results render as 0 marks
(
result.service.ts:68-81).
8. Edge cases (backend-derived)
- Marks exceed maximum →
NotFoundException('Marks cannot exceed maximum.')→ 404RESOURCE_NOT_FOUND, not 422 (examination.service.ts:145-146). Client must pre-validatemarksObtained ≤ maximumMarksand treat a 404 on save as a stale-maximum refresh. - Duplicate subject slot: no server-side uniqueness on
examination_subjects(index{tenantId, examinationId, classId}is not unique,examination-subject.schema.ts:37) and no duplicate check inaddSubject(examination.service.ts:112-131) — the same subject can be added twice; client should warn (OQ-3). - Subject date conflicts: no server check that a subject slot's date/time
collides with another slot or the timetable (
examination.service.tshas no conflict logic) — client-side warning only (OQ-2). - Publish immutability:
publishResultshas no guard — it can be re-called, andPATCH /examinations/:idcan still mutate apublishedexam (theUpdateExaminationDto.statusis a free string,examination.dto.ts:49-52; schema enum is not enforced on thefindOneAndUpdatepath becauseupdateByIdruns withoutrunValidators,base.repository.ts:57-66) — a bad status could persist (OQ-5). Client must treatpublishedas terminal in UI. - Entering marks before a subject exists → 404 "Exam subject not found."
(
examination.service.ts:144). - Report card with no subjects → 404 "No subjects found for this examination."
(
result.service.ts:51-53). - Report card missing results → those subjects count as 0 marks toward totals
(
result.service.ts:70); percentage can be 0 → gradeF(result.service.ts:83-95, 137). - Per-subject
gradeis client-supplied free text (EnterMarksDto.grade,examination-subject.dto.ts:57-60) — never computed server-side per subject; the only computed grade is the report-cardoverallGrade(A+/A/B+/B/C/D/F at 90/80/70/60/50/40,result.service.ts:130-138). passingMarksvsmaximumMarks: both@Min(1)only (examination-subject.dto.ts:36-44);passingMarks > maximumMarksis not rejected — client must validate (OQ-6).- Exam window inversion:
startDate > endDateis not validated (bothIsDateString,examination.dto.ts:19-25);startTime > endTimelikewise not validated (examination-subject.dto.ts:28-34) — client-side validation only (OQ-6). - Type not enum-validated on the wire:
CreateExaminationDto.typeisIsString(examination.dto.ts:13-17) though the schema enum is['midterm','final','unit_test','quarterly','other'](examination.schema.ts:15-20) — invalid values fail onsave()(create path runs validators) but a badtypemay 500 as a validator error; client constrains the picker. - Soft delete:
DELETE /examinations/:id→softDelete(examination.service.ts:99-110,base.repository.ts:68-74) — no cascade to subjects/results; deleted exam → 404 on every read path. Deleted-but-published results remain readable via Results routes. - Pagination:
GET /examinationssupportspage/limit(1–100, default 20), butsortandqare accepted yet ignored by the service (examination.service.ts:67-79usesfind({}, {skip, limit})only) — client-side sorting/filtering required (OQ-7).
9. Assumptions (module)
- Client is forward-looking (shared ledger A1 — PRD Phase 1 is backend; these docs specify the full Flutter client per user instruction).
- Marks entry scope: one
ExaminationResultper (student, subject-slot); the roster of students comes from the Students module (students ofclassIdon the subject slot) — no exam-specific roster endpoint exists. - Exam list is the module home; subject slots are always reached via an exam detail.
exam.*permissions will gate UI ((planned)per blueprint Exams.md:64-72); until the server enforces them, the client gates by role claim defensively but treats the server as authoritative (OQ-4).- Publish is a deliberate, confirmable act; the client never optimistically flips
status to
published(publish has side effects on Results visibility).
10. Open questions (module; global ledger in 00-shared/12)
| # | Item | Impact |
|---|---|---|
| OQ-1 | No exam "active/completed" transitions server-side — status is just a stored string; only create → draft and publish → published are forced (examination.service.ts:48, 209-211). Who flips active/completed and when? | Status picker/stepper UI |
| OQ-2 | Blueprint promises "conflict detection with timetable" (Exams.md:60) but no timetable check exists. Is a 422 BUSINESS_RULE_VIOLATION endpoint planned? | Subject date-conflict UX |
| OQ-3 | Duplicate subject slots are structurally possible (non-unique index). Server fix or client-side dedupe? | Add-subject guard UX |
| OQ-4 | No exam.* permissions seeded (permissions.constants.ts), no RBAC decorators on ExaminationController (examination.controller.ts:21-24). When does RBAC land? | Permission-gated UI visibility |
| OQ-5 | PATCH /examinations/:id accepts a free-string status (examination.dto.ts:49-52) and updateById skips runValidators (base.repository.ts:57-66) — bad status could persist; publish is re-callable. Server fix or client-only immutability? | Post-publish edit rules |
| OQ-6 | passingMarks > maximumMarks, startTime ≥ endTime, startDate > endDate are not server-validated. Client-only constraints? | Form validation strategy |
| OQ-7 | sort/q query params ignored on GET /examinations (examination.service.ts:67-79). Client-side sort/filter fallback? | List UX |
| OQ-8 | MarksEntered/ExaminationCreated/ExaminationUpdated/ExaminationDeleted/ExaminationSubjectAdded have no BullMQ route (event-queue-map.ts) — only ExamResultsPublished fires in-app/results-published. When do notifications land? | Result-published notification UX |
| OQ-9 | Per-subject grade is free text supplied by the client; subjectName in the report card is the raw subjectId string (result.service.ts:75). Are grade normalization and subject-name resolution planned? | Report card rendering |
11. Glossary (this module)
| Term | Meaning |
|---|---|
| Exam (Examination) | examinations doc: academicYearId, name, type, startDate, endDate, status, gradingSchemeId? (examination.schema.ts:9-36) |
| Exam type | midterm, final, unit_test, quarterly, other (examination.schema.ts:15-20) |
| Exam status | draft, active, completed, published (default draft, examination.schema.ts:28-33) |
| Subject slot (ExaminationSubject) | examinations_subjects doc: exam + subject + class + date + time window + maximumMarks/passingMarks (examination-subject.schema.ts:9-31) |
| Mark (ExaminationResult) | examination_results doc: student + slot + marksObtained/grade/remarks/publishedAt (examination-result.schema.ts:9-25) |
| Publish | POST /examinations/:id/publish — stamp publishedAt on all results, set exam status published (examination.service.ts:203-220) |
| Report card | GET /results/report-card/:studentId/:examId aggregate: per-subject rows + totals + percentage + overallGrade (result.service.ts:46-98) |
| Envelope | {success,message,data,meta?,timestamp,requestId} (shared 00-shared/07) |