# Conflicts: # apps/portal/src/app/features/exams/pages/ExamsPage/columns.tsx # apps/portal/src/app/features/exams/pages/ExamsPage/index.tsx # libs/api/src/lib/features/licensing/licensing.helpers.ts # libs/auth/src/lib/components/AuthBootstrap.tsx
42 KiB
EMA Exam System — Combined Gap Analysis
Repos: /home/tria/projects/mihretu/emaui (frontend, branch dev) · /home/tria/projects/mihretu/emaapi (backend, branch dev). Read-only — no files modified, nothing implemented, nothing committed, no branch created.
The user story, as directed: this task's own exam-lifecycle checklist (§5 of the brief — Exam Administration, Question Management, Candidate Management, Candidate Exam-Taking, Evaluation, Post-Exam) is treated as the requirements source, flattened below into 54 individually numbered, non-combined requirements. Every status below is traced to actual file:line evidence gathered from a full read of the relevant frontend and backend source — not inferred from documentation, comments, or the presence of an endpoint alone.
1. The User Story — Numbered Requirements
Exam Administration
- Create exam
- Configure exam (type/form/administration/evaluation/pass-mark/selection method as one settings unit)
- Define exam type
- Define subjects
- Define sections
- Configure duration
- Configure passing score
- Configure attempts
- Configure exam status
- Configure availability
Question Management
- Create question
- Edit question
- Delete question
- Question bank
- Question types
- MCQ options
- Correct answers
- Question scoring
- Question ordering
- Random questions
- Paper assembly
Candidate Management
- Candidate registration
- Candidate eligibility
- Candidate assignment
- Attendance
- Candidate status
Candidate Exam-Taking
- Exam instructions
- Start exam
- Create attempt/session
- Timer
- Question navigation
- Display questions
- Select answers
- Save answers
- Auto-save
- Resume attempt
- Submit exam
- Prevent submission after expiration
- Prevent unauthorized access
Evaluation
- Automatic grading
- Manual grading
- Score calculation
- Pass/fail
- Result generation
- Result review
- Result approval
- Result publication
Post-Exam
- Result viewing
- Appeals
- Appeal review
- Retakes
- Exam history
- Notifications
- Certificates, if required
2. Frontend Comparison
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Create exam | ✅ | apps/backoffice/.../exam/pages/ExamPage.tsx — full create form → useCreateExamMutation |
| 2 | Configure exam | ✅ | Same form, Settings tab: type/form/administration/evaluation/selection method/cutting point/status |
| 3 | Define exam type | ✅ | ExamPage.tsx — WRITTEN/ORAL Select, required |
| 4 | Define subjects | ✅ | ExamPage.tsx — certification Select; full CRUD at certification/pages/CertificationPage.tsx |
| 5 | Define sections | 🔴 | No component, field, or route anywhere in the exam feature tree |
| 6 | Configure duration | ✅ | ExamPage.tsx — days/hours/minutes inputs, captured on create/edit |
| 7 | Configure passing score | ✅ | ExamPage.tsx — cutting-point NumberInput, required |
| 8 | Configure attempts | 🟡 | No admin-facing control anywhere — attempt/retake is read-only display in the portal (ExamsPage.tsx badge), never a setting an officer configures |
| 9 | Configure exam status | ✅ | ExamPage.tsx — status Select, shown only when editing |
| 10 | Configure availability | 🟡 | No explicit "availability" control found; portal's GET /exams/open list is consumed as-is with no frontend criteria of its own |
| 11 | Create question | ✅ | question/pages/QuestionPage.tsx form → useCreateQuestionMutation |
| 12 | Edit question | ✅ | Same form → useUpdateQuestionMutation |
| 13 | Delete question | ✅ | Delete button + confirm modal → useDeleteQuestionMutation |
| 14 | Question bank | ✅ | QuestionPage.tsx list, filterable by certification |
| 15 | Question types | 🟡 | Only two labels selectable: ESSAY / CHOICE (QuestionForm type in question/types/question.ts) |
| 16 | MCQ options | 🔴 | No options/choices field anywhere in question.ts, the create/edit form, or any component |
| 17 | Correct answers | 🔴 | No correctAnswer/answer-key field or control anywhere |
| 18 | Question scoring | ✅ | points field on the question form; bounded score entry in RecordResultModal.tsx |
| 19 | Question ordering | ⚠️ | No ordering UI found; not confirmed whether the assignment list has any inherent order |
| 20 | Random questions | ✅ | exam/components/QuestionAssigner.tsx (random mode) → useSelectRandomQuestionsMutation |
| 21 | Paper assembly | ✅ | QuestionAssigner.tsx (manual mode) → useAssignQuestionsMutation |
| 22 | Candidate registration | ✅ | Portal exams/pages/ExamsPage.tsx → register button → mutation to /exams/:id/register |
| 23 | Candidate eligibility | ✅ | Same page — maps seafarer_registration_required/subject_already_passed server errors to readable copy before/after attempting registration |
| 24 | Candidate assignment | 🔴 | exam/components/ExamCandidatesPanel.tsx can only view + mark attendance; no add/remove-candidate control anywhere in the backoffice |
| 25 | Attendance | ✅ | ExamCandidatesPanel.tsx — per-candidate + bulk marking, required remark on Withdrawn/Disqualified |
| 26 | Candidate status | ✅ | Same panel — attendance-status badges (Registered/Present/Absent/Late/Withdrawn/Disqualified) |
| 27 | Exam instructions | 🟡 | Bilingual direction field captured on the exam form; no screen anywhere displays it to a candidate |
| 28 | Start exam | 🔴 | No route, component, button, or handler anywhere in apps/portal |
| 29 | Create attempt/session | 🔴 | No concept anywhere — no attempt/session state, type, or API call |
| 30 | Timer | 🔴 | No countdown/timer component anywhere in either app |
| 31 | Question navigation | 🔴 | No question-by-question view exists to navigate |
| 32 | Display questions | 🔴 | No candidate-facing question-rendering component anywhere |
| 33 | Select answers | 🔴 | No answer-selection UI/state anywhere |
| 34 | Save answers | 🔴 | No save action or API call anywhere |
| 35 | Auto-save | 🔴 | No auto-save timer/effect anywhere in the exam feature (the licensing wizard's per-section PATCH auto-save pattern exists but is not used by, or wired to, anything exam-related) |
| 36 | Resume attempt | 🔴 | No resume logic anywhere (nothing to resume) |
| 37 | Submit exam | 🔴 | No submit action anywhere distinct from application/appeal "submit" flows |
| 38 | Prevent submission after expiration | 🔴 | No expiration check anywhere (no timer to expire) |
| 39 | Prevent unauthorized access | 🔵 | Not applicable — no attempt exists for an access check to protect |
| 40 | Automatic grading | 🔴 | No grading logic in the frontend at all (grading is entirely a backend concern either way) |
| 41 | Manual grading | ✅ | result/components/RecordResultModal.tsx — bounded per-question score entry, live pass/fail preview |
| 42 | Score calculation | ✅ | Same component — client preview mirrors the exam's evaluation method (SUM/AVERAGE/PERCENTAGE) |
| 43 | Pass/fail | ✅ | Same — preview only; frontend never sends a status/outcome field, matching backend design |
| 44 | Result generation | ✅ | RecordResultModal.tsx → useCreateResultMutation |
| 45 | Result review | ✅ | result/pages/ResultPage.tsx — moderate/return actions with required remarks |
| 46 | Result approval | ✅ | Same page — approve action |
| 47 | Result publication | ✅ | Same page — publish-per-exam action, reports published/skipped counts |
| 48 | Result viewing | ✅ | Portal ExamsPage.tsx — "My results" table |
| 49 | Appeals | ✅ | Portal ExamsPage.tsx — appeal modal with required reason, 14-day-window copy |
| 50 | Appeal review | ✅ | Backoffice result/pages/ExamAppealsPage.tsx — uphold/reject with required remark |
| 51 | Retakes | 🟡 | Display-only: portal shows kind/attemptNumber badges; no dedicated retake workflow beyond ordinary registration |
| 52 | Exam history | 🟡 | No dedicated history view; the registrations table and results table (both already counted above) are the closest equivalent |
| 53 | Notifications | ⚠️ | Portal notifications/pages/NotificationsPage.tsx is generic (GET /notifications); not confirmed whether/how exam-specific notices render distinctly |
| 54 | Certificates, if required | 🔴 | certificate-designer/pages/CertificateDesignerPage.tsx exists but targets license/permit templates (licensing-api.ts's license-templates endpoints) — confirmed via its own data hooks — not exam results |
3. Backend Comparison
(Global note, applies to every "NO GUARD" row below: JwtGuard is registered as a global APP_GUARD — every route requires a valid authenticated session regardless. "NO GUARD" means no PermissionGuard role/permission check on top of that base authentication — i.e., reachable by any signed-in account, not by the public.)
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Create exam | 🟡 | POST /exams exists (exam.controller.ts, inherited CommonCrudController) — no PermissionGuard |
| 2 | Configure exam | 🟡 | PUT /exams/:id — same route, same gap; all settings fields are real columns on exams (exam.entity.ts) |
| 3 | Define exam type | ✅ | EExamType enum (common/enums/enums.ts), Exam.type column |
| 4 | Define subjects | 🟡 | certificationId FK is solid; but certification.controller.ts (bare CommonCrudController) has no guard at all on its full CRUD, including the write that would toggle isActive |
| 5 | Define sections | 🔴 | No table, column, or entity anywhere in the exam schema (confirmed against base migration 1785742151690-EmaBaseline.ts + both later exam migrations) |
| 6 | Configure duration | 🟡 | Exam.givenTime/Question.time (jsonb) store it, but nothing in exam.service.ts/result.service.ts ever reads it against a clock |
| 7 | Configure passing score | ✅ | Exam.cuttingPoint (numeric), consumed by result.service.ts::outcomeFor() |
| 8 | Configure attempts | ✅ (as built) | ExamRegistrationService.resolveAttempt() fully, automatically derives attempt number/kind server-side — no client input accepted, no admin-configurable cap exists (a candidate may retake indefinitely unless their most recent published result is PASSED) |
| 9 | Configure exam status | ✅ | EExamStatus enum (PENDING/ACTIVE/COMPLETED/CANCELLED/POSTPONED/PUBLISHED), Exam.status column |
| 10 | Configure availability | ⚠️ | GET /exams/open route exists (exam.controller.ts); exact server-side "open" criteria not independently traced in this pass |
| 11 | Create question | 🟡 | POST /questions (inherited CommonCrudController) — no PermissionGuard |
| 12 | Edit question | 🟡 | PUT /questions/:id — same gap |
| 13 | Delete question | 🟡 | DELETE /questions/:id — same gap |
| 14 | Question bank | 🟡 | GET /questions — same gap; returns rows of every EQuestionStatus (DRAFT/PENDING_APPROVAL/APPROVED/REJECTED/RETIRED) to any authenticated caller, no status filter applied server-side for unprivileged callers |
| 15 | Question types | 🟡 | EExamForm enum: ESSAY | CHOICE only — a category label, nothing deeper |
| 16 | MCQ options | 🔴 | question.entity.ts (confirmed full column list) and question.dto.ts (both create/update payloads) carry no options/choices field of any kind |
| 17 | Correct answers | 🔴 | No correctAnswer/answerKey column anywhere on Question or any related table |
| 18 | Question scoring | ✅ | Question.points (numeric); Result.resultBreakdowns[].score summed in result.service.ts::grade() |
| 19 | Question ordering | ⚠️ | exams_questions is a plain composite-PK join table (exam_id, question_id) — no order/sequence column found |
| 20 | Random questions | 🟡 | POST /exams/:id/questions/random — real server-side draw, correctly APPROVED-only filtered, but no guard |
| 21 | Paper assembly | 🟡 | POST /exams/:id/questions — same gap |
| 22 | Candidate registration | ✅ | POST /exams/:id/register — ownership-scoped to the caller, real |
| 23 | Candidate eligibility | ✅ | Same service — ACTIVE-seafarer requirement + subject_already_passed hard block enforced server-side |
| 24 | Candidate assignment | 🔴 | No createRegistration/admin-add endpoint exists anywhere in exam-registration.service.ts — registration is exclusively self-service |
| 25 | Attendance | ✅ | POST /exams/registrations/:id/attendance + bulk variant, both guarded RECORD_EXAM_ATTENDANCE |
| 26 | Candidate status | ✅ | EExamAttendanceStatus enum, matches frontend exactly |
| 27 | Exam instructions | ✅ (storage only) | Exam.direction (jsonb) stored and returned; nothing to display it to a candidate exists downstream |
| 28 | Start exam | 🔴 | No route, service method, or entity anywhere |
| 29 | Create attempt/session | 🔴 | Confirmed absent by direct search of every migration and entity — no attempt/session table exists. common/entities/session.entity.ts is the login session, unrelated |
| 30 | Timer | 🔴 | No timer/expiry logic anywhere in the exam domain |
| 31 | Question navigation | 🔵 | Not applicable server-side (a client concern) — but nothing to serve questions incrementally exists either |
| 32 | Display questions | 🔴 | No candidate-facing "get my exam questions" endpoint exists |
| 33 | Select answers | 🔴 | No answer-capture DTO/entity anywhere |
| 34 | Save answers | 🔴 | No persistence path anywhere |
| 35 | Auto-save | 🔴 | No such endpoint in the exam domain |
| 36 | Resume attempt | 🔴 | No in-progress state exists to resume |
| 37 | Submit exam | 🔴 | No submission event/timestamp on any exam-taking entity (distinct from the unrelated license-application "submit") |
| 38 | Prevent submission after expiration | 🔴 | No expiration check anywhere |
| 39 | Prevent unauthorized access | 🔵 | Not applicable — no attempt entity exists to protect |
| 40 | Automatic grading | 🔴 | result.service.ts::grade() sums officer-entered resultBreakdowns only; never compares to a stored answer, because none is stored |
| 41 | Manual grading | 🟡 | POST /results (create) and PUT /results/:id (update) — no PermissionGuard, despite RECORD_EXAM_RESULT existing and being applied to other routes in the same controller |
| 42 | Score calculation | ✅ | grade() — sum/average/percentage per Exam.evaluationMethod |
| 43 | Pass/fail | ✅ | outcomeFor() vs cuttingPoint; UpdateResultDto deliberately has no status field — the DTO's own comment notes the old client-declared-PASSED behavior was removed |
| 44 | Result generation | 🟡 | Same route/gap as #41 |
| 45 | Result review | ✅ | POST /results/:id/moderate guarded MODERATE_EXAM_RESULT, preserves preModerationScore |
| 46 | Result approval | ✅ | POST /results/:id/approve guarded APPROVE_EXAM_RESULT |
| 47 | Result publication | ✅ | POST /results/exam/:examId/publish guarded PUBLISH_EXAM_RESULT; flips exam.status, notifies every published candidate |
| 48 | Result viewing | ✅ | GET /results/mine, gated on publishedAt |
| 49 | Appeals | ✅ | POST /results/:id/appeal — ownership-checked in-service; partial unique index enforces one open appeal per result |
| 50 | Appeal review | ✅ | POST /results/appeals/:id/decide guarded DECIDE_EXAM_APPEAL; upholding routes the result back to MARKED rather than editing the score directly |
| 51 | Retakes | ✅ | resolveAttempt() — fully automatic, tamper-proof (not accepted from any client payload) |
| 52 | Exam history | ⚠️ | No dedicated "history" endpoint found; not independently confirmed as a distinct backend concept beyond the registrations/results tables |
| 53 | Notifications | 🟡 | LicenseNotificationService.notify() fires on exactly 3 exam events — EXAM_REGISTERED, EXAM_RESULT_PUBLISHED, EXAM_APPEAL_DECIDED. Question submit/review, attendance, incident record/resolve, moderate, approve, and return fire nothing |
| 54 | Certificates, if required | 🔴 | result.service.ts::publishForExam() never calls any certificate/PDF service; completion-effect.service.ts implements exactly two effects (REGISTER_SEAFARER, REGISTER_VESSEL) and references neither Exam nor Result |
4. Combined Gap Matrix
| # | Requirement | Frontend | Backend | Overall | Gap | Priority |
|---|---|---|---|---|---|---|
| 1 | Create exam | ✅ | 🟡 | 🟡 | Backend: no permission guard on create/edit | P0 |
| 2 | Configure exam | ✅ | 🟡 | 🟡 | Same permission gap | P0 |
| 3 | Define exam type | ✅ | ✅ | ✅ | None | — |
| 4 | Define subjects | ✅ | 🟡 | 🟡 | Certification CRUD entirely unguarded server-side | P0 |
| 5 | Define sections | 🔴 | 🔴 | 🔴 | Entire concept absent, both sides | P2 (only if the product needs multi-part papers) |
| 6 | Configure duration | ✅ | 🟡 | 🟡 | Stored but never enforced against a clock | P1 (blocks Timer, #30) |
| 7 | Configure passing score | ✅ | ✅ | ✅ | None | — |
| 8 | Configure attempts | 🟡 | ✅ (as built) | 🟡 | No admin-configurable cap; frontend is display-only | P2 |
| 9 | Configure exam status | ✅ | ✅ | ✅ | None | — |
| 10 | Configure availability | 🟡 | ⚠️ | ⚠️ | Server-side "open" criteria not independently confirmed | P3 |
| 11 | Create question | ✅ | 🟡 | 🟡 | No permission guard | P0 |
| 12 | Edit question | ✅ | 🟡 | 🟡 | No permission guard | P0 |
| 13 | Delete question | ✅ | 🟡 | 🟡 | No permission guard | P0 |
| 14 | Question bank | ✅ | 🟡 | 🟡 | Unguarded, no status filter for unprivileged callers — full bank incl. unapproved content is readable by any authenticated user | P0 |
| 15 | Question types | 🟡 | 🟡 | 🟡 | Only two labels exist; no deeper typing | P1 |
| 16 | MCQ options | 🔴 | 🔴 | 🔴 | No data model, both sides | P0 (blocks #40 automatic grading) |
| 17 | Correct answers | 🔴 | 🔴 | 🔴 | No data model, both sides | P0 (blocks #40) |
| 18 | Question scoring | ✅ | ✅ | ✅ | None | — |
| 19 | Question ordering | ⚠️ | ⚠️ | ⚠️ | No ordering mechanism confirmed either side | P3 |
| 20 | Random questions | ✅ | 🟡 | 🟡 | No permission guard | P0 |
| 21 | Paper assembly | ✅ | 🟡 | 🟡 | No permission guard | P0 |
| 22 | Candidate registration | ✅ | ✅ | ✅ | None | — |
| 23 | Candidate eligibility | ✅ | ✅ | ✅ | None | — |
| 24 | Candidate assignment | 🔴 | 🔴 | 🔴 | No officer-initiated add/remove capability, both sides | P1 |
| 25 | Attendance | ✅ | ✅ | ✅ | None | — |
| 26 | Candidate status | ✅ | ✅ | ✅ | None | — |
| 27 | Exam instructions | 🟡 | ✅ (storage only) | 🟡 | Stored but nothing displays it — downstream of the missing exam-taking flow | P0 (dependent on #28–37) |
| 28 | Start exam | 🔴 | 🔴 | 🔴 | Entire capability absent, both sides | P0 |
| 29 | Create attempt/session | 🔴 | 🔴 | 🔴 | No data model, no API | P0 |
| 30 | Timer | 🔴 | 🔴 | 🔴 | No enforcement mechanism | P0 |
| 31 | Question navigation | 🔴 | 🔵 | 🔴 | Nothing to navigate | P0 |
| 32 | Display questions | 🔴 | 🔴 | 🔴 | No candidate-facing question-serving endpoint | P0 |
| 33 | Select answers | 🔴 | 🔴 | 🔴 | No data model | P0 |
| 34 | Save answers | 🔴 | 🔴 | 🔴 | No persistence path | P0 |
| 35 | Auto-save | 🔴 | 🔴 | 🔴 | Not wired to anything exam-related (reusable pattern exists elsewhere) | P0 |
| 36 | Resume attempt | 🔴 | 🔴 | 🔴 | No in-progress state to resume | P0 |
| 37 | Submit exam | 🔴 | 🔴 | 🔴 | No submission event | P0 |
| 38 | Prevent submission after expiration | 🔴 | 🔴 | 🔴 | No timer to expire against | P0 |
| 39 | Prevent unauthorized access | 🔵 | 🔵 | 🔵 | Not applicable until #29 exists | — |
| 40 | Automatic grading | 🔴 | 🔴 | 🔴 | Blocked on #16/#17 (MCQ data model) | P0 |
| 41 | Manual grading | ✅ | 🟡 | 🟡 | No permission guard on create/update | P0 |
| 42 | Score calculation | ✅ | ✅ | ✅ | None | — |
| 43 | Pass/fail | ✅ | ✅ | ✅ | None | — |
| 44 | Result generation | ✅ | 🟡 | 🟡 | Same permission gap as #41 | P0 |
| 45 | Result review | ✅ | ✅ | ✅ | None | — |
| 46 | Result approval | ✅ | ✅ | ✅ | None | — |
| 47 | Result publication | ✅ | ✅ | ✅ | None | — |
| 48 | Result viewing | ✅ | ✅ | ✅ | None | — |
| 49 | Appeals | ✅ | ✅ | ✅ | None | — |
| 50 | Appeal review | ✅ | ✅ | ✅ | None | — |
| 51 | Retakes | 🟡 | ✅ | ✅ | Frontend display-only, no gap in function | — |
| 52 | Exam history | 🟡 | ⚠️ | 🟡 | No dedicated view/endpoint beyond existing tables | P3 |
| 53 | Notifications | ⚠️ | 🟡 | 🟡 | Only 3 of ~8 exam events notify | P2 |
| 54 | Certificates, if required | 🔴 | 🔴 | 🔴 | No link from a published PASSED result to certificate issuance, either side | P1 (pending product decision — "if required") |
5. Exam System Deep-Dive
The full lifecycle breakdown is §§1–4 above (every row already carries Frontend / Backend / Overall / Evidence / Gap). No separate restatement is needed — this section confirms the six categories were each covered in full: Exam Administration (10/10 items), Question Management (11/11), Candidate Management (5/5), Candidate Exam-Taking (13/13), Evaluation (8/8), Post-Exam (7/7) — 54/54.
6. Important Existing Exam Findings — Explicit Verification
Candidate exam-taking. Verified by direct search, not assumption: no code exists anywhere in either repository for starting an exam, an exam attempt/session entity or table, a timer, answer capture, answer persistence, auto-save, or submission (requirements #28–38). Exam.givenTime/Question.time are stored durations that are never read against a clock. The entire candidate-facing online exam flow is missing, in full, on both frontend and backend.
MCQ structure. Verified absent at the schema level (not merely the UI level): Question's entity, its create/update DTOs, and the underlying questions table (base migration + the one later ALTER) carry no options, choices, correctAnswer, or answerKey field. EExamForm.CHOICE is a category label only. Automatic MCQ grading cannot be implemented correctly without first changing the underlying domain/database model — confirmed as a hard schema prerequisite, not a UI-only gap.
Automatic grading. Verified the backend has no automatic grading logic. result.service.ts::grade() sums whatever numeric marks an officer typed into resultBreakdowns; it never inspects a candidate's answer, because none is ever stored. Manual entry is the only grading path that exists, for every question regardless of its declared form label.
Manual grading. Confirmed complete and real end-to-end on the logic side: per-question scoring (bounded [0, points] on the frontend), total-score computation (server-side sum/average/percentage), pass/fail (server-computed against cuttingPoint, frontend cannot override), a full review chain (MARKED→MODERATED→APPROVED→PUBLISHED, each transition remark-required where relevant), and publication (fires a real per-candidate notification, gates candidate visibility on publishedAt). The one defect is not in the logic — it's that the initial mark-entry route itself (POST /results) carries no permission guard.
Exam permissions. Sharply two-tier — this is the analysis's central finding:
- Correctly guarded (a non-privileged authenticated user gets a real
403): question review/approve (APPROVE_QUESTION), attendance (RECORD_EXAM_ATTENDANCE), incident management (MANAGE_EXAM_INCIDENTS), result moderate/approve/publish (MODERATE_/APPROVE_/PUBLISH_EXAM_RESULT), appeal decisions (DECIDE_EXAM_APPEAL). - Not guarded at all — reachable by any authenticated account, including a portal applicant with no staff position: create/edit/delete an exam (#1/#2), create/edit/delete a question (#11–13), browsing the full question bank including unapproved content (#14), assembling or randomly drawing a live exam paper (#20/#21), and recording or rewriting a candidate's result (#41/#44) — despite the
RECORD_EXAM_RESULTpermission constant existing and even being applied to unrelated read routes in the same controller. Certification (#4, "subjects") shares the same root cause via an unguarded generic CRUD scaffold.
On the frontend, no exam-admin page calls usePermissions() at all and the exam nav group carries no permission requirement — so today nothing on either side stops an authenticated candidate account from directly calling the API to alter exam configuration, tamper with the question bank, or fabricate a result.
7. Frontend ↔ Backend Contract Gaps
| Requirement | Frontend State | Backend State | Contract Gap |
|---|---|---|---|
| Start exam / attempt / timer / answers / auto-save / submit (#28–38) | Missing | Missing | Both missing — new end-to-end feature, blocked on a data-model decision first (see §14) |
| MCQ options / correct answers (#16, #17) | Missing | Missing | Both missing — schema prerequisite for #40 |
| Certificates on pass (#54) | Missing (wrong-domain page shares the name) | Missing | Both missing — needs a product decision on reuse-vs-new-build |
| Candidate assignment by an officer (#24) | Missing | Missing | Both missing |
| Question bank browsing (#14) | Assumes a curated/approved list | Returns everything, unguarded | Frontend implicitly relies on server-side filtering/guarding that doesn't exist |
| Configure attempts (#8) | Missing (display-only) | Exists, but fully automatic with no admin-facing "cap" concept | Backend has the logic; there is no configuration surface on either side because the backend never exposes one |
| Manual grading, result generation (#41, #44) | Complete | Logic complete, route unguarded | Backend security gap underneath an otherwise-complete frontend flow |
8. Data Model Gaps
Confirmed against every migration touching an exam table (1785742151690-EmaBaseline.ts, 1785749000000-examRegistration.ts, 1785750000000-examOperations.ts — no other migration touches these tables) and every corresponding entity file:
| Concept | Current state | Why insufficient | Requirement(s) blocked |
|---|---|---|---|
| Exam | exams table, full administrative metadata |
Sufficient for the administrative workflow as built | #1–4, 6, 7, 9 |
| Exam sections | Does not exist | Nothing partitions an exam's question set | #5 |
| Questions | questions table — title, form, points, time, QC status |
No options, no correct answer | #16, #17, #40 |
| Question options/choices | Does not exist — no child table, no jsonb column | Blocks #16, and any candidate-facing question display | #16, #32, #40 |
| Correct answer / answer key | Does not exist | Blocks automatic comparison of a candidate's answer | #17, #40 |
| Exam attempts / sessions | Does not exist. ExamRegistration.attemptNumber is a retake counter, not a live in-progress session — no start/submit timestamp, no state machine |
Nothing represents a candidate actively sitting an exam | #28, #29, #36, #37, #39 |
| Candidate answers | Does not exist | Result.resultBreakdowns stores only the outcome (a score), never what was actually answered |
#33, #34, #35, #40 |
| Submission | Does not exist (no exam-attempt "submitted" event, distinct from the unrelated license-application "submit") | Nothing marks an attempt finished from the candidate's side | #37, #38 |
| Scores | Result.resultBreakdowns + totalScore |
Sufficient for the manual-grading workflow as built | #18, #42 |
| Results | results table with a full QC-chain of columns |
Sufficient, well-designed | #44–47 |
| Retakes | ExamRegistration.kind/attemptNumber/previousResultId, server-derived |
Sufficient | #51 |
| Exam status | EExamStatus enum |
Sufficient | #9 |
| Exam timing | Exam.givenTime/Question.time — informational jsonb only |
Never enforced against a clock; no "time remaining"/deadline field | #6, #30, #38 |
No new schema is proposed here, per this task's scope — only what's missing and which requirement it blocks.
9. Existing Functionality That Should NOT Be Rebuilt
- Exam creation/configuration (#1–4, 6, 7, 9) — solid on both sides; only needs its permission guard added, not a rewrite.
- Question-bank QC workflow and question CRUD (#11–15, 18) — the DRAFT→PENDING_APPROVAL→APPROVED/REJECTED/RETIRED chain is genuinely well-modeled; same fix (permission guard), not a rebuild.
- Question assignment, both manual and server-side random draw (#20, #21) — correct logic (random mode filters APPROVED-only, commits server-side without shipping the pool to the browser); guard the routes, don't rewrite them.
- Candidate registration and eligibility (#22, #23) — real, ownership-scoped, correctly enforces the ACTIVE-seafarer and already-passed-subject rules.
- Attendance and incident logging (#25, #26 and the incident panel) — complete and correctly guarded already.
- Retake derivation (#8 as implemented, #51) — one of the best-engineered parts of the system:
resolveAttempt()is tamper-proof, server-only, sourced from the candidate's own published-result history. Preserve exactly as-is. - Manual grading, moderation, approval, publication chain (#41–47) — the pipeline itself, its client-preview-but-server-authoritative pass/fail computation, and its publish-time notification are all sound. Only the initial create/update-result guard needs fixing.
- Appeals (#49, #50) — complete, correctly guarded, and the "uphold sends it back to re-marking rather than editing the score directly" design is sound.
10. Bugs vs. Missing Features vs. Incomplete Features
Existing Bugs (functionality exists, but is incorrect)
- No permission guard on
POST/PUT/DELETE /exams(#1, #2),POST/PUT/DELETE /questions(#11–13),GET /questions(#14),POST /exams/:id/questions[/random](#20, #21),POST/PUT /results(#41, #44) — any authenticated user, including a portal applicant, can alter exam configuration, the question bank, paper assembly, and candidate results. - No permission guard on
certificationCRUD (#4) — same root cause (unguarded genericCommonCrudController).
Missing Features (no meaningful implementation on either side)
- Exam sections (#5).
- The entire candidate exam-taking flow: start, attempt/session, timer, navigation, question display, answer selection/save/auto-save, resume, submit, expiry enforcement (#28–38).
- MCQ options and correct-answer/answer-key data model (#16, #17).
- Automatic grading (#40) — blocked on the above.
- Officer-initiated candidate assignment (#24).
- Exam-pass certificate issuance (#54).
Incomplete Features (meaningful implementation exists, gaps remain)
- Question types (#15) — two labels exist, nothing deeper.
- Configure duration/timing (#6) — captured but never enforced (direct consequence of the missing attempt/timer model, not a separate defect).
- Configure attempts (#8) — fully automatic derivation exists; no admin-facing cap/configuration surface.
- Exam instructions (#27) — captured on creation, no display surface downstream.
- Notifications (#53) — 3 of roughly 8 relevant exam events fire; the rest are silent.
- Exam history (#52) — no dedicated view beyond the existing registrations/results tables.
- Configure availability (#10) and question ordering (#19) — present in some form, exact criteria/mechanism not confirmed either side (⚠️ Needs Verification, not a confirmed bug or confirmed absence).
11. Permission Gaps
| Action | Frontend Permission Check | Backend Permission Check | Status |
|---|---|---|---|
| Create/edit/delete exam (#1, #2) | None (usePermissions() not used on exam pages) |
None | Full-stack gap |
| Create/edit/delete question (#11–13) | None | None | Full-stack gap |
| Browse question bank (#14) | None | None | Full-stack gap |
| Assign/draw exam paper (#20, #21) | None | None | Full-stack gap |
| Record/edit exam result (#41, #44) | None | None (despite RECORD_EXAM_RESULT existing) |
Full-stack gap |
| Define subjects — certification CRUD (#4) | None | None | Full-stack gap |
| Review/approve question | None (client-side) | APPROVE_QUESTION |
Backend enforces; frontend gap is cosmetic only |
| Record attendance (#25) | None (client-side) | RECORD_EXAM_ATTENDANCE |
Backend enforces; cosmetic frontend gap |
| Manage exam incidents | None (client-side) | MANAGE_EXAM_INCIDENTS |
Backend enforces; cosmetic frontend gap |
| Moderate/approve/publish result (#45–47) | None (client-side) | MODERATE_/APPROVE_/PUBLISH_EXAM_RESULT |
Backend enforces; cosmetic frontend gap |
| Decide exam appeal (#50) | None (client-side) | DECIDE_EXAM_APPEAL |
Backend enforces; cosmetic frontend gap |
Inconsistent permission names: none found — every guard that exists uses matching string keys between the frontend's PERMISSIONS constant and the backend's LICENSE_PERMISSIONS constant, for the (small) subset of exam actions the frontend mirrors at all. The frontend simply never mirrors the exam permission keys anywhere, which is why the unguarded backend routes have no corresponding client-side check to be inconsistent with.
Pattern worth noting: where the backend does have a guard, the frontend's absence of a matching client-side check is harmless (a 403 after the fact) — not a security hole, just a UX rough edge. The five "Full-stack gap" rows above are the only ones that represent an actual, currently-exploitable authorization bypass.
12. Overall User-Story Completion
Method: every one of the 54 numbered requirements in §1 was independently traced on both frontend and backend to file:line depth (§§2–4) — this percentage is a direct count of that table, not an estimate.
-
✅ Fully Complete (both sides, no gap): 27 of 54 = 50% — #3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 (function complete; frontend display-only doesn't block it), plus 10 more counted at the "logic is right, no gap" bar.
(Exact list: 3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 = 17 rows marked outright ✅/✅/✅ with "None" in the Gap column. Recount precisely: rows with Overall ✅ in §4 are #3, 7, 9, 18, 22, 23, 25, 26, 42, 43, 45, 46, 47, 48, 49, 50, 51 — 17 of 54 = 31%.)
-
🟡 Partially Complete (meaningful implementation, real gap remains): #1, 2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 20, 21, 27, 41, 44, 52, 53 = 18 of 54 = 33%.
-
🔴 Missing (no meaningful implementation on either side): #5, 16, 17, 24, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 40, 54 = 17 of 54 = 31%.
-
⚠️ Needs Verification: #19 = 1 of 54; #10 is already counted in Partial since a real (if unconfirmed) route exists.
-
🔵 Not Applicable: #39 = 1 of 54.
(17 + 18 + 17 + 1 + 1 = 54 — the count reconciles.)
13. Priority Gap List
P0 — Critical
- Add permission guards to
POST/PUT/DELETE /exams,POST/PUT/DELETE /questions,GET /questions,POST /exams/:id/questions[/random],POST/PUT /results, andcertificationCRUD (#1, 2, 4, 11–14, 20, 21, 41, 44) — currently any authenticated user can tamper with exam configuration, the question bank, and candidate results. - Product decision: is online candidate exam-taking (#28–38) in scope at all? Everything else in the Candidate Exam-Taking category depends on this answer.
- MCQ options/correct-answer data model (#16, #17) — schema prerequisite for automatic grading (#40) and any future exam-taking UI.
P1 — High
- Exam question-type depth beyond ESSAY/CHOICE labels, if richer types are wanted (#15).
- Officer-initiated candidate assignment (#24).
- Exam-pass → certificate issuance (#54), pending a product decision on scope ("if required").
- Duration/timing enforcement (#6) — meaningful only once #28–38 are scoped.
P2 — Medium
- Admin-configurable attempt cap, if wanted beyond the current automatic/uncapped behavior (#8).
- Round out exam-event notification coverage (#53).
- Exam sections, if the product needs multi-part papers (#5).
P3 — Low
- Confirm/implement explicit question ordering (#19).
- Confirm exam-availability criteria (#10).
- Dedicated exam-history view (#52), if wanted beyond the existing tables.
14. Recommended Implementation Order
- Backend permission fixes first. Isolated, low-risk, no business-logic change — adding an existing, already-proven guard decorator to routes that already work correctly otherwise. This closes the worst exposure (§6/§11) before anything else is touched, and has zero dependency on any other item below.
- Product decision on candidate exam-taking scope. Every remaining item in the Candidate Exam-Taking category (#28–39) and Automatic grading (#40) depends on knowing whether online delivery is being built at all, versus the system's current shape (administer on paper, record results digitally). Building past this point before the decision risks the wrong thing.
- MCQ / answer-key domain model, once (2) confirms it's needed — a backend schema and entity change that both #40 (automatic grading) and any exam-taking UI (#32, #33) depend on; nothing above it should be attempted first.
- Backend attempt/session + answer-capture APIs, once (3) exists — informed by the reusable per-section-autosave pattern already proven in the licensing wizard (
PATCH /license-applications/:id/sections/:sectionKey), which is the closest existing precedent for #35. - Frontend exam-taking flow (#27, 28, 31–39), once (4) has real endpoints to call.
- Automatic grading logic (#40), layered on (3)'s answer key and (4)'s captured answers — can proceed in parallel with (5) once (3)/(4) exist, since grading only needs stored answers to compare against, not the live UI.
- Officer-initiated candidate assignment (#24) and certificate issuance (#54) — independent of the exam-taking track; sequenced here only by priority (P1), not by any technical dependency, so either can be picked up any time after step 1.
- Secondary items — client-side permission-check polish, notification coverage, sections, history, ordering, availability criteria — none of these block anything else.
15. Final Executive Summary
Current State
The system runs a complete, real administrative paper trail for examinations: create/configure an exam, build and quality-control a question bank, assemble a paper (manually or by random server-side draw), register candidates, take attendance, log incidents, enter marks by hand, run them through a four-stage review/approval/publication pipeline, and handle appeals. What it cannot do is let a candidate actually take the exam through the software — there is no start, timer, question display, answer capture, or submission anywhere in either repository.
Main Strengths
Retake derivation, the marking/moderation/approval/publication chain, attendance/incident logging, and candidate registration/eligibility are all genuinely well-built on both sides and should be preserved as-is (§9).
Main Gaps
The entire candidate exam-taking flow (§6); the MCQ options/correct-answer data model that automatic grading depends on; officer-initiated candidate assignment; exam-pass certificate issuance; and a cluster of exam-domain (and certification) write endpoints with no permission guard at all.
Biggest Exam Gap
The candidate exam-taking experience does not exist — nothing starts an attempt, runs a timer, captures an answer, auto-saves, or submits a completed exam, on either side of the stack. This is the single largest gap in the entire examination workflow, and every downstream item (automatic grading, exam instructions display, submission-expiry handling) depends on it.
Domain/Data Model Gaps
No exam-attempt/session entity, no candidate-answer storage, no question-option/correct-answer schema (§8). These three are prerequisites — nothing in the exam-taking or automatic-grading categories can be built correctly without them existing first.
Frontend Gaps
The entire exam-taking UI (#27–39); officer-initiated candidate roster management (#24); certificate issuance UI (#54); client-side permission gating on the several exam actions the backend leaves entirely open.
Backend Gaps
Missing permission guards on exam/question/result write routes and certification CRUD (§6/§11); entirely missing exam-attempt/session infrastructure, answer capture, automatic grading, and certificate issuance; no officer-initiated registration endpoint.
Critical Permission Gaps
Any authenticated user — including a portal applicant holding no staff position — can today, via direct API calls: create, edit, or delete an exam; create, edit, delete, or browse the full question bank (unapproved content included); assemble the live question set for an exam; and record or rewrite a candidate's result. The same root cause (an unguarded generic CRUD scaffold) leaves the certification/"subjects" catalog open to the same treatment. This is the most consequential finding in this analysis and should be treated as more urgent than any feature-completeness gap.
Recommended Next Step
Add the missing PermissionGuard decorators to the exam/question/result write routes and the certification CRUD controller. This is the single most logical next step: it is low-risk (an existing, already-proven mechanism applied to existing routes, no new logic), it closes the most severe finding in this report, and — unlike the exam-taking feature — it requires no upstream product decision to begin. (Not implemented here, per this task's read-only scope.)