feat: add payment functionality for passed exam certificates and update related statuses

This commit is contained in:
Nati
2026-08-21 06:55:51 +00:00
parent 357dc3e4d5
commit cc4ce06a71
4 changed files with 49 additions and 8 deletions

View File

@@ -36,6 +36,7 @@ import {
useGetMyMedicalCertificatesQuery, useGetMyMedicalCertificatesQuery,
} from '@ema-platform/api'; } from '@ema-platform/api';
import { PdfPreviewModal } from '@ema-platform/ui'; import { PdfPreviewModal } from '@ema-platform/ui';
import { useApplicationPayment } from '../../payments/hooks/useApplicationPayment';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mock data // Mock data
@@ -55,6 +56,9 @@ interface CertificatesOverview {
type: string; type: string;
submitted: string; submitted: string;
status: 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<string | null>(null); const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [previewTitle, setPreviewTitle] = useState(''); const [previewTitle, setPreviewTitle] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { pay, isPaying } = useApplicationPayment();
const { data } = useApiQuery<CertificatesOverview>({ const { data } = useApiQuery<CertificatesOverview>({
url: '/certificates/my', url: '/certificates/my',
@@ -313,14 +318,28 @@ export function CertificatesPage() {
</Badge> </Badge>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text <Group gap="xs" justify="flex-end" wrap="nowrap">
fz="xs" {/* feeAmount is non-null exactly when the server would
c="blue" accept a payment, so the button and the API agree. */}
style={{ cursor: 'pointer' }} {app.feeAmount !== null && (
onClick={() => navigate(`/applications/${app.applicationId}`)} <Button
> size="xs"
Details color="yellow"
</Text> loading={isPaying}
onClick={() => pay(app.applicationId)}
>
Pay {app.feeAmount.toLocaleString()} {app.feeCurrency}
</Button>
)}
<Text
fz="xs"
c="blue"
style={{ cursor: 'pointer' }}
onClick={() => navigate(`/applications/${app.applicationId}`)}
>
Details
</Text>
</Group>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
))} ))}

View File

@@ -93,6 +93,25 @@ export function ExamStageActions({
); );
} }
// Passed: the certificate itself is the last fee on the application.
if (app.status === 'EXAM_PASSED') {
return (
<Button
size="xs"
variant="filled"
color="yellow"
loading={paying}
onClick={() => onPay(app)}
>
{t('applications.actions.payCertificateFee', {
defaultValue: 'Pay certificate fee ({{amount}} {{currency}})',
amount: Number(app.feeAmount ?? 0).toLocaleString(),
currency: app.feeCurrency,
})}
</Button>
);
}
// Paid and scheduled are both waiting states — nothing for the candidate to // Paid and scheduled are both waiting states — nothing for the candidate to
// do, so say so rather than offering a button that does nothing. // do, so say so rather than offering a button that does nothing.
if (app.status === 'EXAM_PAID' || app.status === 'EXAM_SCHEDULED') { if (app.status === 'EXAM_PAID' || app.status === 'EXAM_SCHEDULED') {

View File

@@ -14,6 +14,7 @@ const EXAM_STAGE_STATUSES = [
'EXAM_PAID', 'EXAM_PAID',
'EXAM_SCHEDULED', 'EXAM_SCHEDULED',
'EXAM_FAILED', 'EXAM_FAILED',
'EXAM_PASSED',
]; ];
export function applicationActionsColumn( export function applicationActionsColumn(

View File

@@ -159,6 +159,8 @@ export const APPLICANT_ACTION_STATUSES: LicenseStatus[] = [
// sit again after a failure. // sit again after a failure.
'EXAM_PAYMENT_PENDING', 'EXAM_PAYMENT_PENDING',
'EXAM_FAILED', '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. */ /** Statuses that are finished, whichever way they went. */