01 — Product Overview (Results Module)
- 1. Purpose
- 2. Scope
- 3. Out of scope (Phase 1 backend)
- 4. Key behavioural facts from source
- 5. Success metrics
- 6. Open questions / gaps
StudyLyon — multi-tenant ERP / School Management API. This package designs the Results 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/results/**,src/modules/exams/**,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 and marked(planned)/(forward-looking)/(proposed)inline. The PRD keeps the mobile client out of Phase 1 — this entire package is the(forward-looking)client design against the v1 API.
1. Purpose
Results is the grading and reporting layer of the academic cycle. Teachers enter
marks per (exam-subject, student), the backend computes totals, percentage, and
an overall grade per report card, and publication releases results to stakeholders.
The module owns the examination_results collection (examination-result.schema.ts:7-8)
but marks are written by the Exams module service (examination.service.ts:176-182);
the Results controller proxies marks entry into that service (result.controller.ts:26-31).
| Responsibility | Source |
|---|---|
| List results for one student | result.controller.ts:16-20 → result.service.ts:36-38 |
| List results for one exam-subject | result.controller.ts:21-25 → result.service.ts:40-44 |
| Enter/update marks (idempotent upsert) | result.controller.ts:26-31 → examination.service.ts:139-195 |
| Report card per (student, exam) | result.controller.ts:32-37 → result.service.ts:46-98 |
| Publish all results of an exam | examination.controller.ts:54-56 → examination.service.ts:203-220 |
2. Scope
- Marks entry per exam-subject, one student at a time, with idempotent re-entry (upsert,
examination.service.ts:147-175). - Result lookup by student and by exam-subject.
- Report card generation: per-subject marks, totals, percentage (2 decimals,
result.service.ts:83-86), overall grade via deterministic bands (result.service.ts:130-138). - Publication of an exam's results, which stamps
publishedAton every result row and flips the exam tostatus: 'published'(examination.service.ts:203-220). - Notification of publication via
ExamResultsPublished→in-appqueue /results-publishedjob (event-queue-map.ts:27).
3. Out of scope (Phase 1 backend)
- Student/parent portal visibility — no per-role visibility filter exists in code; every endpoint is behind
JwtAuthGuardonly (result.controller.ts:10). BlueprintResults.md"before publish, results hidden" is not implemented (see 4.2). - Rank computation — blueprint
Results.md:54says "Rank computed per class per exam", but no rank logic exists inresult.service.ts.(planned) - Analytics — blueprint lists
GET /api/v1/results/analytics(Results.md:28); no controller route exists.(proposed) - PDF report card export — blueprint assigns it to the Reports module (
Results.md:57); no implementation.(planned) - Mobile/Flutter client — the PRD keeps mobile out of Phase 1; this spec is the forward-looking client design against the v1 API.
(forward-looking) - RBAC permissions —
permissions.constants.ts:1-97contains noresult.*orexam.*permissions; the blueprint'sresult.read/compute/publish/report(Results.md:62-69) are unimplemented.(planned)
4. Key behavioural facts from source
- Marks upsert is idempotent:
examination.service.ts:147-150looks up by(studentId, examinationSubjectId); existing row is updated in place (:152-161), otherwise created (:176-182). A client can safely re-submit. - Unique index:
{tenantId, studentId, examinationSubjectId}unique (examination-result.schema.ts:31-33) — the DB-level backstop for the upsert. - Missing marks count as 0 in the report card (
result.service.ts:70result?.marksObtained ?? 0), buttotalMaximumMarksstill sums every subject (:72). A student with no entries is graded F, not excluded. - Over-maximum marks throw 404 (
examination.service.ts:145-146,NotFoundException) — a quirk: business-rule violations should be 422BUSINESS_RULE_VIOLATION, but this path returns 404RESOURCE_NOT_FOUND. The client must treat both as "marks rejected" (see 14_QA_Checklist.md). - Publication is not an immutability lock:
publishResults(examination.service.ts:203-220) only stampspublishedAtand sets exam status; marks can still be upserted afterwards. There is no "frozen" check inenterMarks. - Overall grade bands (
result.service.ts:130-138): A+ ≥ 90, A ≥ 80, B+ ≥ 70, B ≥ 60, C ≥ 50, D ≥ 40, F < 40 — only the overall grade is computed server-side. Per-subjectgradeis client-supplied (optional string inEnterMarksDto,examination-subject.dto.ts:57-60) and stored verbatim (examination.service.ts:157). - Report card subject name is the subject ID:
subjectName: sub.subjectId.toString()(result.service.ts:75) — no populate/join; the client must resolve display names locally.
5. Success metrics
- Marks entry converges in ≤ 2 attempts (idempotent upsert → no duplicate/conflict errors).
- Report card always renders even with missing marks (0-fill) or zero subjects (404 → guided empty state).
- Publish action visibly stamps results and fires the in-app
results-publishednotification.
6. Open questions / gaps
| # | Gap | Status |
|---|---|---|
| 1 | No role-based visibility: students/parents can't be restricted to their own results | (planned) — RBAC guards not yet implemented (AGENTS.md) |
| 2 | publishedAt is never filtered on read paths (examination-result.repository.ts:20-40) | (planned) |
| 3 | Per-subject grade computed? Currently client-sent; blueprint implies server grading | Open question |
| 4 | Rank/percentile per class per exam | (planned) |
| 5 | Bulk marks entry (grid POST) — only per-student enterMarks exists | (planned) — /:id/marks-import in IMPLEMENTATION_PLAN.md:219 |
| 6 | Analytics endpoint | (proposed) |