diff --git a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
index bcb410228..f253d8e16 100644
--- a/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
+++ b/apps/backoffice/src/app/features/license-review/pages/LicenseReviewPage/index.tsx
@@ -34,6 +34,7 @@ import {
IconPaperclip,
IconPencil,
IconQuestionMark,
+ IconRefresh,
IconX,
} from "@tabler/icons-react";
import { notifications } from "@mantine/notifications";
@@ -71,6 +72,7 @@ import {
useScheduleInspectionMutation,
useRescheduleInspectionMutation,
useGetCertificateUrlForOfficerMutation,
+ useRegenerateCertificateMutation,
uploadDocument,
type ApplicationRemark,
type RemarkTargetType,
@@ -236,6 +238,8 @@ export function LicenseReviewPage() {
const [scheduleIssuance] = useScheduleIssuanceMutation();
const [issueCertificate] = useIssueCertificateMutation();
const [getCertificateUrlForOfficer] = useGetCertificateUrlForOfficerMutation();
+ const [regenerateCertificate, { isLoading: regeneratingCertificate }] =
+ useRegenerateCertificateMutation();
const [certificateBusy, setCertificateBusy] = useState<"view" | "download" | null>(
null,
);
@@ -720,6 +724,37 @@ export function LicenseReviewPage() {
}
}
+ /**
+ * Re-renders the stored certificate from whatever design is published now.
+ * Unlike View/Download above, this overwrites the stored PDF even though
+ * one already exists — the fix for a certificate printed from a design
+ * that turned out to be wrong (e.g. a placeholder published for a rank),
+ * where View/Download would otherwise keep handing back the same bad file.
+ */
+ async function handleRegenerateCertificate() {
+ if (!app.issuedLicenseId) return;
+ try {
+ await regenerateCertificate(app.issuedLicenseId).unwrap();
+ notifications.show({
+ color: "teal",
+ title: t("review.certificateRegenerated", "Certificate regenerated"),
+ message: t(
+ "review.certificateRegeneratedBody",
+ "Re-rendered from the currently published design.",
+ ),
+ });
+ } catch (err) {
+ notifications.show({
+ color: "red",
+ title: t(
+ "review.certificateRegenerateError",
+ "Could not regenerate the certificate",
+ ),
+ message: extractErrorMessage(err),
+ });
+ }
+ }
+
/** Opens the booking modal seeded with the visit already on the books. */
function openReschedule() {
if (!pendingInspection) return;
@@ -1156,6 +1191,29 @@ export function LicenseReviewPage() {
>
{t("review.download", "Download")}
+ {/* Same authority as issuing in the first place — reprints
+ the stored PDF from whatever design is published now,
+ for when the one on file turns out to be wrong. */}
+ {can(["can:issue:license-certificate"]) && (
+
+ }
+ loading={regeneratingCertificate}
+ disabled={Boolean(certificateBusy) || regeneratingCertificate}
+ onClick={handleRegenerateCertificate}
+ >
+ {t("review.regenerateCertificate", "Regenerate")}
+
+
+ )}
)}
diff --git a/libs/api/src/lib/features/licensing/licensing-api.ts b/libs/api/src/lib/features/licensing/licensing-api.ts
index 523f519cd..f82461c47 100644
--- a/libs/api/src/lib/features/licensing/licensing-api.ts
+++ b/libs/api/src/lib/features/licensing/licensing-api.ts
@@ -763,6 +763,17 @@ export const licensingApi = baseApi
query: (id) => ({ url: `/licenses/${id}/certificate-backoffice` }),
}),
+ /**
+ * Force re-renders a certificate that already has a stored PDF, from
+ * whatever design is published now — the two routes above only fill in
+ * a *missing* PDF, so a licence printed from a design that turns out to
+ * be wrong (a placeholder someone published, a broken background image)
+ * has no other way to pick up a fix once the design is corrected.
+ */
+ regenerateCertificate: builder.mutation<{ url: string }, string>({
+ query: (id) => ({ url: `/licenses/${id}/regenerate-certificate`, method: 'POST' }),
+ }),
+
// ------------------------------------------------------------- review
getQueue: builder.query, QueueFilter | void>({
query: (params) => ({
@@ -1479,6 +1490,7 @@ export const {
useGetLicensesQuery,
useGetCertificateUrlMutation,
useGetCertificateUrlForOfficerMutation,
+ useRegenerateCertificateMutation,
useGetApplicationPaymentQuery,
usePatchSectionMutation,
useAddStaffMutation,