12 — API Mapping (Exams Module)
- 0. Module-wide request envelope & client policy
- 1. S1 — Exams List
- 2. S2 — Exam Detail
- 3. S3 — Marks Entry
- 4. S4 — Add Subject Slot
- 5. S5 — Publish
- 6. S6 — Student Results (mine)
- 7. S7 — Report Card
- 8. S8 / S8a — Create / Edit Exam
- 9. Events & queues (context, not client endpoints)
- 10. Client error mapping (module)
Exact wire contract for every screen → endpoint. Base
/api/v1(main.ts:44global prefix); envelope per 00-shared/07_API_Conventions.md. All endpoints fromexamination.controller.tsandresult.controller.ts; business rules fromexamination.service.tsandresult.service.ts. All endpoints are guarded byJwtAuthGuardonly (examination.controller.ts:21-24,result.controller.ts:7-10); no per-endpoint RBAC decorators exist yet (OQ-4).
0. Module-wide request envelope & client policy
| Aspect | Contract |
|---|---|
| Base | https://api.<domain>/api/v1 |
| Headers | Authorization: Bearer <accessToken>; x-request-id; Content-Type: application/json; Idempotency-Key (UUID) on mark saves |
| Success | {success, message:'OK', data, meta?, timestamp, requestId} (response-envelope.interceptor.ts:11-18, 47-60) |
| Error | {success, message, error:{code, details?}, timestamp, requestId} (http-exception.filter.ts:16-25, 73-81) |
| Tenancy | tenantId from JWT claim — never in body (00-shared/07 §6) |
| Pagination | page (1-based), limit (1–100, default 20), sort/q accepted but ignored by exams list (OQ-7); meta {page,limit,totalItems,totalPages,hasNext,hasPrevious} (pagination-query.dto.ts:5-30, 41-55) |
| Caching | lists: last-good cache TTL 24 h; marks/coverage: 5 min SWR (00-shared/06 §3.3) |
| Offline | reads cached; mark writes queued with idempotency keys (13_State_Management.md §4); create/edit/publish online-only |
| Retry | backoff on 5xx/network; no auto-retry on 429 |
1. S1 — Exams List
| Endpoint | GET /examinations?page=1&limit=20 (examination.controller.ts:30-32) |
| Request | PaginationQueryDto {page?, limit?, sort?, q?} (pagination-query.dto.ts:5-30) — sort/q ignored by service (OQ-7) |
| Success | 200 → data = ExaminationDocument[], meta = pagination meta (examination.service.ts:67-79; buildPaginationMeta, pagination-query.dto.ts:41-55) |
| Doc shape | `_id, tenantId, academicYearId, name, type('midterm' |
| Errors | 400 bad page/limit; 401; 429; 5xx |
| Source | examination.service.ts:67-79 |
| Offline | cached list (24 h) |
2. S2 — Exam Detail
| Endpoint | GET /examinations/:id (examination.controller.ts:33-35) |
| Success | 200 → data = exam doc (shape above) |
| Errors | 404 RESOURCE_NOT_FOUND "Examination not found." (examination.service.ts:61-65); cross-tenant/deleted → same 404 |
| Endpoint | GET /examinations/:id/subjects (examination.controller.ts:51-53) |
| Success | 200 → data = ExaminationSubjectDocument[] (examination.service.ts:133-137; examination-subject.repository.ts:20-24) — unpaginated |
| Slot shape | _id, examinationId, subjectId, classId, date, startTime('HH:mm'), endTime('HH:mm'), maximumMarks, passingMarks (examination-subject.schema.ts:9-31) |
| Endpoint (coverage) | GET /results/exam-subject/:examSubjectId (result.controller.ts:21-25) → data = ExaminationResultDocument[] (marked count per slot) |
| Result shape | _id, studentId, examinationSubjectId, marksObtained?, grade?, remarks?, publishedAt?, version, createdAt, updatedAt (examination-result.schema.ts:9-25) |
| Errors | 404s as above; 429 |
| Offline | cached header + slots (slots 5 min) |
3. S3 — Marks Entry
| Endpoint | POST /results/exam-subject/:examSubjectId/marks (result.controller.ts:26-31) |
| Request | EnterMarksDto {studentId, marksObtained, grade?, remarks?} (examination-subject.dto.ts:47-66) |
| Success | 201 (create) or 200 (update) → data = result doc; upsert by (tenantId, studentId, examinationSubjectId) unique index (examination-result.schema.ts:31-33); existing doc updated in place (examination.service.ts:147-175); emits MarksEntered {studentId, examinationSubjectId} (examination.service.ts:163-174, 183-193) |
| Business rules | marksObtained > subject.maximumMarks → 404 NotFoundException('Marks cannot exceed maximum.') (examination.service.ts:145-146) — client pre-validates (see 08 §4); slot missing → 404 "Exam subject not found." (examination.service.ts:144); no check of passingMarks, of student↔class membership, or of publish state (OQ-5) |
| Errors | 400 VALIDATION_ERROR (bad ids / negative marks); 404 as above; 429; 5xx |
| Source | examination.service.ts:139-195 |
| Offline | queued with Idempotency-Key = op id; flush FIFO |
4. S4 — Add Subject Slot
| Endpoint | POST /examinations/:id/subjects (examination.controller.ts:45-50) |
| Request | CreateExaminationSubjectDto {examinationId, subjectId, classId, date(ISO), startTime, endTime, maximumMarks, passingMarks} (examination-subject.dto.ts:11-45); examinationId injected from route (examination.service.ts:49) |
| Success | 201 → data = slot doc; emits ExaminationSubjectAdded (examination.service.ts:119-130) |
| Business rules | @Min(1) on both marks fields only; no server check: date/time overlap (OQ-2), duplicate subject (OQ-3), passingMarks ≤ maximumMarks (OQ-6), window ⊆ exam (OQ-6) |
| Errors | 400; 404 exam missing (create fails on repository); 429 |
| Source | examination.service.ts:112-131 |
5. S5 — Publish
| Endpoint | POST /examinations/:id/publish (examination.controller.ts:54-56) |
| Success | 200 → data = void (examination.service.ts:203-220); server stamps publishedAt = now on all results of the exam's slots (examination-result.repository.ts:42-51), sets exam status: 'published' (examination.service.ts:209-211); emits ExamResultsPublished {examinationId, subjectCount} (examination.service.ts:212-219) → queue in-app, job results-published (event-queue-map.ts:27) |
| Business rules | no guards: publishes whatever marks exist (no completeness check); re-callable; does not lock edits (OQ-5); not a DB transaction (two sequential writes) |
| Errors | 404 exam missing (updateById no-op on missing id — client treats as refresh); 429 |
| Source | examination.service.ts:203-220 |
6. S6 — Student Results (mine)
| Endpoint | GET /results/student/:studentId (result.controller.ts:16-20) |
| Success | 200 → data = ExaminationResultDocument[] — unpaginated (result.service.ts:36-38; examination-result.repository.ts:26-28); no publish filter — unpublished marks are returned |
| Errors | 401; 429 |
| Note | client groups rows by exam via slot ids (subject/exam names resolved client-side, OQ-9) |
7. S7 — Report Card
| Endpoint | GET /results/report-card/:studentId/:examId (result.controller.ts:32-37) |
| Success | 200 → data = ReportCard {studentId, examinationId, subjects:[{subjectId, subjectName(==raw subjectId, OQ-9), marksObtained, maximumMarks, grade?, remarks?}], totalMarksObtained, totalMaximumMarks, percentage(2dp), overallGrade, generatedAt(ISO)} (result.service.ts:8-26, 46-98) |
| Business rules | no subjects → 404 "No subjects found for this examination." (result.service.ts:51-53); missing marks count as 0 (result.service.ts:70); overallGrade bands A+(≥90)/A(≥80)/B+(≥70)/B(≥60)/C(≥50)/D(≥40)/F (result.service.ts:130-138) |
| Errors | 404 as above; 429 |
8. S8 / S8a — Create / Edit Exam
| Endpoint | POST /examinations (examination.controller.ts:27-29) |
| Request | CreateExaminationDto {academicYearId, name, type, startDate, endDate} (examination.dto.ts:4-26); server forces status:'draft' (examination.service.ts:48); emits ExaminationCreated {examinationId, name} (examination.service.ts:50-57) |
| Success | 201 → data = exam doc |
| Endpoint | PATCH /examinations/:id (examination.controller.ts:36-41) |
| Request | UpdateExaminationDto {name?, type?, startDate?, endDate?, status?} (examination.dto.ts:28-53); $set only sent fields, version +1 (base.repository.ts:57-66); emits ExaminationUpdated {examinationId, changes:[keys]} (examination.service.ts:88-96) |
| Errors | 400; 404 "Examination not found." (examination.service.ts:85-87); free-string status not enum-enforced on PATCH (OQ-5) |
| Endpoint (delete) | DELETE /examinations/:id (examination.controller.ts:42-44) — soft delete (examination.service.ts:99-110, base.repository.ts:68-74); emits ExaminationDeleted; no cascade to slots/results; 404 if already deleted |
9. Events & queues (context, not client endpoints)
| Item | Source |
|---|---|
ExamResultsPublished → queue in-app, job results-published | event-queue-map.ts:27 |
ExaminationCreated/Updated/Deleted, ExaminationSubjectAdded, MarksEntered — emitted but no BullMQ route (no consumers today) | examination.service.ts:50-57, 88-96, 102-110, 119-130, 163-174, 183-193; event-queue-map.ts |
ExaminationResultCreated (Results module path) | result.service.ts:114-126 |
Queues available (emails, push, whatsapp, in-app, …) | queue.constants.ts:1-17 |
Planned exams endpoints: /:id/schedule, /hall-tickets, /seating-plan, /marks-import, /re-evaluation | (planned) docs/IMPLEMENTATION_PLAN.md:213-220 |
Mock test / DPP / test series (createMockTest, createDPP, createTestSeries, exam type enum extension) | (planned) docs/IMPLEMENTATION_PLAN.md:501-567, 643 |
10. Client error mapping (module)
| Code | Exams-specific UX |
|---|---|
400 VALIDATION_ERROR | inline field errors (08 §5); bulk → pre-validate client-side |
401 UNAUTHENTICATED | refresh once; else session expiry (00-shared/06 §5) |
403 PERMISSION_DENIED | hide plan/publish/marks actions; read-only state (RBAC (planned), OQ-4) |
404 RESOURCE_NOT_FOUND | two special cases: "Marks cannot exceed maximum." → refresh slot max + re-enter (examination.service.ts:146); "Exam subject not found." → pop to detail (examination.service.ts:144); others → purge cache + return to list |
409 DUPLICATE_RESOURCE | not expected today (no unique constraints on exams/slots, OQ-3); offline flush collision preview |
422 BUSINESS_RULE_Violation | reserved for future conflict/immutability checks — not emitted by exams service today (OQ-2/OQ-5) |
429 RATE_LIMITED | backoff, no auto-retry |
| 5xx | generic + requestId; retry; last-good cache |