fix(exam): show the back office the real examination state

The COC detail page read the application status alone, so a candidate who had
registered, been marked present, sat the paper and had a PASSED mark published
still appeared to be awaiting an exam — and the officer was offered "Schedule
exam" and "Record exam result" for work already done.

ExamStatePanel reads GET /exams/applications/:id/state, which the server
derives from the registration, the attempt and the published mark. Session,
date and admission number while the exam is ahead; score, outcome and whether
the exam engine or an examiner produced the mark once it is settled. Polled,
because attendance is recorded and papers are marked while the officer has the
page open. Same derivation the applicant's portal reads, so the two cannot
disagree.

The eligible-exams query, the scheduling mutation and the outcome mutation are
gone from the RTK layer along with the endpoints behind them.
This commit is contained in:
mihretue
2026-08-31 08:04:56 +00:00
parent 6f4878d300
commit ee64856983
6 changed files with 204 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
import { baseApi } from '../../base-api';
import type {
ExamStateView,
AppNotification,
ApplicationDetail,
ApplicationKind,
@@ -649,6 +650,18 @@ export const licensingApi = baseApi
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
}),
/**
* The examination leg behind one application, as the server reads it.
*
* The COC detail page used to infer this from the application status
* alone, which is how it kept offering "Schedule exam" for a paper the
* candidate had already sat and passed.
*/
getExamStateForApplication: builder.query<ExamStateView, string>({
query: (id) => ({ url: `/exams/applications/${id}/state` }),
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
}),
// No `getEligibleExams`, `scheduleExam` or `recordExamOutcome`: the
// endpoints behind them are gone. Sittings are published as master
// schedules and candidates register for one themselves, and an outcome
@@ -1053,6 +1066,7 @@ export const {
useUpdateLicenseValidityMutation,
useGetLicenseTypeRequirementsQuery,
useCreateApplicationMutation,
useGetExamStateForApplicationQuery,
useGetMyApplicationsQuery,
useGetApplicationQuery,
useInitiatePaymentMutation,

View File

@@ -748,3 +748,36 @@ export interface IssuedLicense {
certificateFileKey: string | null;
}
/**
* Where a certificate application stands in the examination leg, derived
* server-side from the registration, the attempt and the published mark
* (ExamStateService). The application status alone cannot answer this — which
* is why the back office could show "awaiting exam" over a published pass.
*/
export type ExamState =
| 'NOT_REGISTERED'
| 'REGISTERED'
| 'ATTENDANCE_CONFIRMED'
| 'NOT_SITTING'
| 'IN_PROGRESS'
| 'UNDER_EVALUATION'
| 'PASSED'
| 'FAILED';
export interface ExamStateView {
state: ExamState;
registrationId: string | null;
admissionNumber: string | null;
examId: string | null;
examTitle: { en?: string; am?: string } | null;
examDate: string | null;
attendanceStatus: string | null;
attemptStatus: 'IN_PROGRESS' | 'SUBMITTED' | 'EXPIRED' | null;
/** Only once the mark is published — before that it is not the candidate's. */
score: number | null;
outcome: 'PASSED' | 'FAILED' | null;
publishedAt: string | null;
/** Whether the exam engine produced the mark, or a person did. */
autoGraded: boolean | null;
}