diff --git a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx index 9e8879996..73e924775 100644 --- a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx @@ -36,6 +36,7 @@ import { useGetMyMedicalCertificatesQuery, } from '@ema-platform/api'; import { PdfPreviewModal } from '@ema-platform/ui'; +import { useApplicationPayment } from '../../payments/hooks/useApplicationPayment'; // --------------------------------------------------------------------------- // Mock data @@ -55,6 +56,9 @@ interface CertificatesOverview { type: string; submitted: string; status: string; + /** The fee owed at the current status, or null when nothing is due. */ + feeAmount: number | null; + feeCurrency: string | null; }[]; } @@ -141,6 +145,7 @@ export function CertificatesPage() { const [previewUrl, setPreviewUrl] = useState(null); const [previewTitle, setPreviewTitle] = useState(''); const [loading, setLoading] = useState(false); + const { pay, isPaying } = useApplicationPayment(); const { data } = useApiQuery({ url: '/certificates/my', @@ -313,14 +318,28 @@ export function CertificatesPage() { - navigate(`/applications/${app.applicationId}`)} - > - Details - + + {/* feeAmount is non-null exactly when the server would + accept a payment, so the button and the API agree. */} + {app.feeAmount !== null && ( + + )} + navigate(`/applications/${app.applicationId}`)} + > + Details + + ))} diff --git a/apps/portal/src/app/features/licensing/components/ExamStageActions.tsx b/apps/portal/src/app/features/licensing/components/ExamStageActions.tsx index 76ceebaa4..b19ec09f4 100644 --- a/apps/portal/src/app/features/licensing/components/ExamStageActions.tsx +++ b/apps/portal/src/app/features/licensing/components/ExamStageActions.tsx @@ -93,6 +93,25 @@ export function ExamStageActions({ ); } + // Passed: the certificate itself is the last fee on the application. + if (app.status === 'EXAM_PASSED') { + return ( + + ); + } + // Paid and scheduled are both waiting states — nothing for the candidate to // do, so say so rather than offering a button that does nothing. if (app.status === 'EXAM_PAID' || app.status === 'EXAM_SCHEDULED') { diff --git a/apps/portal/src/app/features/licensing/pages/MyApplicationsPage/actions.tsx b/apps/portal/src/app/features/licensing/pages/MyApplicationsPage/actions.tsx index a2d078363..523a8c6ba 100644 --- a/apps/portal/src/app/features/licensing/pages/MyApplicationsPage/actions.tsx +++ b/apps/portal/src/app/features/licensing/pages/MyApplicationsPage/actions.tsx @@ -14,6 +14,7 @@ const EXAM_STAGE_STATUSES = [ 'EXAM_PAID', 'EXAM_SCHEDULED', 'EXAM_FAILED', + 'EXAM_PASSED', ]; export function applicationActionsColumn( diff --git a/libs/api/src/lib/features/licensing/licensing.helpers.ts b/libs/api/src/lib/features/licensing/licensing.helpers.ts index f4dcc1c57..248de738b 100644 --- a/libs/api/src/lib/features/licensing/licensing.helpers.ts +++ b/libs/api/src/lib/features/licensing/licensing.helpers.ts @@ -160,6 +160,8 @@ export const APPLICANT_ACTION_STATUSES: LicenseStatus[] = [ // sit again after a failure. 'EXAM_PAYMENT_PENDING', 'EXAM_FAILED', + // Passed: the certificate fee falls due, and only the candidate can pay it. + 'EXAM_PASSED', ]; /** Statuses that are finished, whichever way they went. */