[STU-103] Inspector ID verification approve/reject workflow#27
Conversation
…obile layout - Expand candidate edit form with additional profile fields - Add WorkLogStaffActions component for staff work log management - Extend WorkLogAppealForm with improved UX - Add mobile-responsive workspace shell layout (tab bar, rail collapse) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add server actions (approveIdRequest, rejectIdRequest) with Zod validation
for rejection reason, client component with confirm/reason UI, rejection
reason display in fact panel, and toast notifications. Auth-gated via
requireRoleCapability("inspector", "id_review.mutate").
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedPull request was closed or merged during review Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
WalkthroughThe PR extends ID request inspector UI to display rejection reasons conditionally, tightens approval/rejection status validation, refactors certificate and education data shapes in the candidate detail layer, narrows certificate upload input, and introduces mobile-responsive layout and panel styling throughout the application. ChangesID Request Inspector & Candidate Form Updates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/inspector/id-requests/[id]/page.tsx (1)
20-42:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRender
IdRequestActionsin the page body.
IdRequestActionsis imported but never mounted, so inspectors cannot approve/reject from this screen.Suggested fix
<WorkspaceShell session={session} eyebrow="Inspector / ID Request" title={`ID request ${data.request.cir_uuid.slice(0, 18)}`} metrics={data.metrics} primary={{ title: "Candidates", rows: data.candidates }} > <FactPanel title="Batch" facts={[ { label: "Status", value: data.request.status }, { label: "Created By", value: data.request.staff_candidate_id_request_created_byTostaff?.staff_name }, { label: "Updated By", value: data.request.staff_candidate_id_request_updated_byTostaff?.staff_name }, { label: "Created", value: formatDate(data.request.created_at) }, { label: "Updated", value: formatDate(data.request.updated_at) }, { label: "Raw Candidate IDs", value: data.request.candidate_ids }, ...(data.request.status === "rejected" && data.request.rejection_reason ? [{ label: "Rejection reason", value: data.request.rejection_reason }] : []) ]} /> + <IdRequestActions requestUuid={data.request.cir_uuid} currentStatus={data.request.status} /> </WorkspaceShell>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/inspector/id-requests/`[id]/page.tsx around lines 20 - 42, IdRequestActions is imported but never mounted, so add it into the page render inside the WorkspaceShell children (for example directly above or below the FactPanel) so inspectors can approve/reject; render <IdRequestActions request={data.request} /> (or the props IdRequestActions expects) using the existing data.request object and keep it within the WorkspaceShell children alongside FactPanel to ensure the actions appear on the page.
🧹 Nitpick comments (1)
src/modules/candidates/WorkLogAppealForm.tsx (1)
32-100: ⚡ Quick winAvoid keeping two
WorkLogStaffActionssources.This component is duplicated in
src/modules/candidates/WorkLogStaffActions.tsx. Keeping both implementations will drift; keep one canonical component and re-export it here if needed.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/candidates/WorkLogAppealForm.tsx` around lines 32 - 100, There are two copies of the WorkLogStaffActions component which will drift; remove this duplicated implementation and instead re-export the canonical WorkLogStaffActions implementation from the single source of truth (the other file that currently contains WorkLogStaffActions). Keep any referenced helper symbols (approveWorkLog, rejectWorkLog, useActionState) in the canonical module, and update this file to only export or re-export WorkLogStaffActions (or replace its body with a simple export { WorkLogStaffActions } from '...canonical source...') and update any imports elsewhere to point to the canonical export.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/styles.css`:
- Around line 10310-10312: The hover rule for .candidateMissingFields li a:hover
uses an undefined token var(--fg) so it is ignored; update that rule to use the
project's defined color token for link hover (replace var(--fg) with the correct
token such as the shared link/text hover variable used across the app, e.g.,
--color-link-hover or --text-hover—whichever exists in your token palette) so
the hover color applies; locate the .candidateMissingFields li a:hover selector
and swap the token accordingly.
In `@src/modules/candidates/actions.ts`:
- Around line 813-814: The current guards only prevent re-applying the same
status by checking request.status === "approved"/"rejected"; change both checks
to enforce transitions only from pending by verifying request.status !==
"pending" and returning an error like "Only pending requests can be
approved/rejected." Update the two spots that currently read e.g. if
(request.status === "approved") ... and the analogous rejection check (the other
block around line 865-866) to use request.status !== "pending" and appropriate
messages so approvals/rejections are only allowed when request.status is
"pending".
- Around line 818-825: The status update and subsequent notification inserts
must be made atomic: locate the blocks (e.g., in approveIdRequest and
rejectIdRequest) that call prisma.candidate_id_request.update and then
prisma.notification.create/createMany (also similar blocks at the other noted
ranges) and wrap the update plus all notification creates inside a single
prisma.$transaction so either both the status change and the notifications
persist or neither do; replace standalone awaits with a single transaction call
that includes the update operation and the notification insert operations
together, preserving the same data (status, updated_by, updated_at, notification
payloads) and error handling.
In `@src/modules/candidates/WorkLogAppealForm.tsx`:
- Around line 104-113: The success toast is skipped on the first successful
resolution because prevError is initialized to state.error, so the "previous
error existed" check fails; change the prevError initialization and the
useEffect checks: initialize prevError.current to null/undefined instead of
state.error and in the effect compare prevError.current (null vs string) so that
when !pending and state.error === "" and prevError.current is a non-empty string
(or not null) you call toast.success; update references to prevError,
state.error, pending, and the useEffect callback accordingly.
In `@src/modules/candidates/WorkLogStaffActions.tsx`:
- Around line 43-45: The badge currently interpolates `currentStatus` directly
which can render "Status null"; in the WorkLogStaffActions component update the
span text logic to explicitly handle null (e.g., `currentStatus === null ? "No
status" : currentStatus === 1 ? "Approved" : currentStatus === 2 ? "Rejected" :
\`Status ${currentStatus}\``) so null shows a user-friendly label; keep the
existing data-status attribute as-is or convert to a string if needed, but
ensure `currentStatus === null` is checked before falling back to the generic
`Status ${currentStatus}`.
- Around line 20-37: The success-toast logic currently requires a prior
non-empty error (prevApproveError/prevRejectError), so first-time successful
approve/reject never shows success; change the effects to detect a transition
from pending true to false with no error instead: in the approve effect, use a
prevApprovePending ref and show success when prevApprovePending.current === true
&& !approvePending && approveState.error === ""; likewise add prevRejectPending
and in the reject effect show success when prevRejectPending.current === true &&
!rejectPending && rejectState.error === ""; update prevApprovePending.current
and prevRejectPending.current at the end of their effects and keep updating
prevApproveError/prevRejectError as needed.
---
Outside diff comments:
In `@src/app/inspector/id-requests/`[id]/page.tsx:
- Around line 20-42: IdRequestActions is imported but never mounted, so add it
into the page render inside the WorkspaceShell children (for example directly
above or below the FactPanel) so inspectors can approve/reject; render
<IdRequestActions request={data.request} /> (or the props IdRequestActions
expects) using the existing data.request object and keep it within the
WorkspaceShell children alongside FactPanel to ensure the actions appear on the
page.
---
Nitpick comments:
In `@src/modules/candidates/WorkLogAppealForm.tsx`:
- Around line 32-100: There are two copies of the WorkLogStaffActions component
which will drift; remove this duplicated implementation and instead re-export
the canonical WorkLogStaffActions implementation from the single source of truth
(the other file that currently contains WorkLogStaffActions). Keep any
referenced helper symbols (approveWorkLog, rejectWorkLog, useActionState) in the
canonical module, and update this file to only export or re-export
WorkLogStaffActions (or replace its body with a simple export {
WorkLogStaffActions } from '...canonical source...') and update any imports
elsewhere to point to the canonical export.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 41e73ea4-1b51-4a4a-a198-e7cca2501681
📒 Files selected for processing (12)
prisma/schema.prismasrc/app/candidate/edit/page.tsxsrc/app/inspector/id-requests/[id]/IdRequestActions.tsxsrc/app/inspector/id-requests/[id]/page.tsxsrc/app/styles.csssrc/modules/candidates/CandidateEditForm.tsxsrc/modules/candidates/CandidateProfile.tsxsrc/modules/candidates/WorkLogAppealForm.tsxsrc/modules/candidates/WorkLogStaffActions.tsxsrc/modules/candidates/actions.tssrc/modules/workspace/NoticeToast.tsxsrc/modules/workspace/data.ts
| await prisma.candidate_id_request.update({ | ||
| where: { cir_uuid: requestUuid }, | ||
| data: { | ||
| status: "approved", | ||
| updated_by: staffId, | ||
| updated_at: now, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Make status update and notifications atomic.
Each action updates candidate_id_request first, then inserts notifications. If notification insert fails, status is still mutated and the workflow becomes partially applied.
Suggested fix
- await prisma.candidate_id_request.update({
- where: { cir_uuid: requestUuid },
- data: {
- status: "approved",
- updated_by: staffId,
- updated_at: now,
- },
- });
-
- const candidateIds = parseCandidateIdList(request.candidate_ids);
- if (candidateIds.length > 0) {
- await prisma.candidate_notification.createMany({
- data: candidateIds.map((candidateId) => ({
- cn_uuid: crypto.randomUUID(),
- candidate_id: candidateId,
- type: 50,
- staff_id: staffId,
- message: "Your ID verification request has been approved.",
- is_new: true,
- created_at: now,
- updated_at: now,
- })),
- });
- }
+ const candidateIds = parseCandidateIdList(request.candidate_ids);
+ await prisma.$transaction(async (tx) => {
+ await tx.candidate_id_request.update({
+ where: { cir_uuid: requestUuid },
+ data: {
+ status: "approved",
+ updated_by: staffId,
+ updated_at: now,
+ },
+ });
+
+ if (candidateIds.length > 0) {
+ await tx.candidate_notification.createMany({
+ data: candidateIds.map((candidateId) => ({
+ cn_uuid: crypto.randomUUID(),
+ candidate_id: candidateId,
+ type: 50,
+ staff_id: staffId,
+ message: "Your ID verification request has been approved.",
+ is_new: true,
+ created_at: now,
+ updated_at: now,
+ })),
+ });
+ }
+ });Apply the same transaction pattern in rejectIdRequest.
Also applies to: 827-841, 870-878, 880-894
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/candidates/actions.ts` around lines 818 - 825, The status update
and subsequent notification inserts must be made atomic: locate the blocks
(e.g., in approveIdRequest and rejectIdRequest) that call
prisma.candidate_id_request.update and then
prisma.notification.create/createMany (also similar blocks at the other noted
ranges) and wrap the update plus all notification creates inside a single
prisma.$transaction so either both the status change and the notifications
persist or neither do; replace standalone awaits with a single transaction call
that includes the update operation and the notification insert operations
together, preserving the same data (status, updated_by, updated_at, notification
payloads) and error handling.
| useEffect(() => { | ||
| if (approveState.error && approveState.error !== prevApproveError.current) { | ||
| toast.error("Approval failed", { description: approveState.error }); | ||
| } else if (!approvePending && approveState.error === "" && prevApproveError.current !== "") { | ||
| toast.success("Work log approved", { description: "The work log entry has been approved." }); | ||
| } | ||
| prevApproveError.current = approveState.error; | ||
| }, [approveState.error, approvePending]); | ||
|
|
||
| useEffect(() => { | ||
| if (rejectState.error && rejectState.error !== prevRejectError.current) { | ||
| toast.error("Rejection failed", { description: rejectState.error }); | ||
| } else if (!rejectPending && rejectState.error === "" && prevRejectError.current !== "") { | ||
| toast.success("Work log rejected", { description: "The work log entry has been rejected." }); | ||
| setMode("idle"); | ||
| } | ||
| prevRejectError.current = rejectState.error; | ||
| }, [rejectState.error, rejectPending]); |
There was a problem hiding this comment.
Approve/reject success toasts can be missed on first success.
Lines 23 and 32 require a prior non-empty error, so first-time successful approve/reject submits won't show success toasts.
Proposed fix
const prevApproveError = useRef(approveState.error);
const prevRejectError = useRef(rejectState.error);
+ const prevApprovePending = useRef(false);
+ const prevRejectPending = useRef(false);
useEffect(() => {
- if (approveState.error && approveState.error !== prevApproveError.current) {
- toast.error("Approval failed", { description: approveState.error });
- } else if (!approvePending && approveState.error === "" && prevApproveError.current !== "") {
- toast.success("Work log approved", { description: "The work log entry has been approved." });
+ if (prevApprovePending.current && !approvePending) {
+ if (approveState.error) {
+ toast.error("Approval failed", { description: approveState.error });
+ } else {
+ toast.success("Work log approved", { description: "The work log entry has been approved." });
+ }
}
prevApproveError.current = approveState.error;
+ prevApprovePending.current = approvePending;
}, [approveState.error, approvePending]);
useEffect(() => {
- if (rejectState.error && rejectState.error !== prevRejectError.current) {
- toast.error("Rejection failed", { description: rejectState.error });
- } else if (!rejectPending && rejectState.error === "" && prevRejectError.current !== "") {
- toast.success("Work log rejected", { description: "The work log entry has been rejected." });
- setMode("idle");
+ if (prevRejectPending.current && !rejectPending) {
+ if (rejectState.error) {
+ toast.error("Rejection failed", { description: rejectState.error });
+ } else {
+ toast.success("Work log rejected", { description: "The work log entry has been rejected." });
+ setMode("idle");
+ }
}
prevRejectError.current = rejectState.error;
+ prevRejectPending.current = rejectPending;
}, [rejectState.error, rejectPending]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/candidates/WorkLogStaffActions.tsx` around lines 20 - 37, The
success-toast logic currently requires a prior non-empty error
(prevApproveError/prevRejectError), so first-time successful approve/reject
never shows success; change the effects to detect a transition from pending true
to false with no error instead: in the approve effect, use a prevApprovePending
ref and show success when prevApprovePending.current === true && !approvePending
&& approveState.error === ""; likewise add prevRejectPending and in the reject
effect show success when prevRejectPending.current === true && !rejectPending &&
rejectState.error === ""; update prevApprovePending.current and
prevRejectPending.current at the end of their effects and keep updating
prevApproveError/prevRejectError as needed.
DevRel ReviewStatus: Changes requested — 6 actionable CodeRabbit findings + merge conflict Critical:
|
- Mount IdRequestActions in inspector ID request detail page so inspectors can approve/reject requests - Add server-side status guard: only allow approve/reject transitions from "pending" status (previously only checked for already-approved or already-rejected, allowing invalid transitions) - Replace undefined CSS variable var(--fg) with var(--foreground) in .candidateMissingFields li a:hover rule Co-Authored-By: Paperclip <noreply@paperclip.ing>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/modules/candidates/actions.ts (1)
807-841:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winMake the pending-status check part of the write.
Both actions still do a read-then-write sequence, so two inspectors can race: each reads
"pending", then both apply different terminal states and enqueue notifications. Wrap the status change and notification insert in one transaction, and make the mutation itself conditional onstatus: "pending"so only one caller can win.Suggested pattern
- await prisma.candidate_id_request.update({ - where: { cir_uuid: requestUuid }, - data: { - status: "approved", - updated_by: staffId, - updated_at: now, - }, - }); - const candidateIds = parseCandidateIdList(request.candidate_ids); - if (candidateIds.length > 0) { - await prisma.candidate_notification.createMany({ - data: candidateIds.map((candidateId) => ({ - cn_uuid: crypto.randomUUID(), - candidate_id: candidateId, - type: 50, - staff_id: staffId, - message: "Your ID verification request has been approved.", - is_new: true, - created_at: now, - updated_at: now, - })), - }); + const applied = await prisma.$transaction(async (tx) => { + const result = await tx.candidate_id_request.updateMany({ + where: { cir_uuid: requestUuid, status: "pending" }, + data: { + status: "approved", + updated_by: staffId, + updated_at: now, + }, + }); + + if (result.count !== 1) return false; + + if (candidateIds.length > 0) { + await tx.candidate_notification.createMany({ + data: candidateIds.map((candidateId) => ({ + cn_uuid: crypto.randomUUID(), + candidate_id: candidateId, + type: 50, + staff_id: staffId, + message: "Your ID verification request has been approved.", + is_new: true, + created_at: now, + updated_at: now, + })), + }); + } + + return true; + }); + + if (!applied) { + return { error: "This request has already been processed." }; }Apply the same pattern in
rejectIdRequest.Also applies to: 859-894
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/modules/candidates/actions.ts` around lines 807 - 841, Replace the read-then-write for approving the ID request with an atomic transaction that performs a conditional mutation (only where cir_uuid = requestUuid AND status = "pending") and then inserts notifications, so only one caller can win; specifically, remove the initial prisma.candidate_id_request.findUnique and prisma.candidate_id_request.update calls and instead use prisma.$transaction to run prisma.candidate_id_request.updateMany({ where: { cir_uuid: requestUuid, status: "pending" }, data: { status: "approved", updated_by: staffId, updated_at: now } }) and, if updateMany.count > 0, include prisma.candidate_notification.createMany(...) in the same transaction; if updateMany.count is 0 return the same pending-status error. Apply the identical pattern in rejectIdRequest (use updateMany with status filter and conditional notification insert inside a single transaction).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@src/modules/candidates/actions.ts`:
- Around line 807-841: Replace the read-then-write for approving the ID request
with an atomic transaction that performs a conditional mutation (only where
cir_uuid = requestUuid AND status = "pending") and then inserts notifications,
so only one caller can win; specifically, remove the initial
prisma.candidate_id_request.findUnique and prisma.candidate_id_request.update
calls and instead use prisma.$transaction to run
prisma.candidate_id_request.updateMany({ where: { cir_uuid: requestUuid, status:
"pending" }, data: { status: "approved", updated_by: staffId, updated_at: now }
}) and, if updateMany.count > 0, include
prisma.candidate_notification.createMany(...) in the same transaction; if
updateMany.count is 0 return the same pending-status error. Apply the identical
pattern in rejectIdRequest (use updateMany with status filter and conditional
notification insert inside a single transaction).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 2392c597-af77-45ee-838e-81ffc5ea80f1
📒 Files selected for processing (3)
src/app/inspector/id-requests/[id]/page.tsxsrc/app/styles.csssrc/modules/candidates/actions.ts
Resolved conflicts in candidate edit page, styles, CandidateEditForm, WorkLogStaffActions, and actions.ts — keeping STU-103/STU-171 changes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Taking main's CandidateEditForm, WorkLogAppealForm, WorkLogStaffActions, and actions.ts, then re-applying STU-171 status guards on approveIdRequest and rejectIdRequest (only allow transitions from "pending" status). Co-Authored-By: Paperclip <noreply@paperclip.ing>
…ma mismatch - Add educationEntries, degrees, majors to candidate edit page props - Add universityLabel/degreeLabel/majorLabel to educationEntries mapping - Remove certificate_title, certificate_issuer, certificate_url from queries (fields don't exist in Prisma schema) - Remove as WorkLogAction casts on form action props Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ns) [STU-103] Co-Authored-By: Paperclip <noreply@paperclip.ing>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/modules/candidates/actions.ts`:
- Around line 498-499: The current certificateSchema's certificate_type uses
z.string().transform((v) => v === "true").pipe(z.boolean()) which silently
coerces any non-"true" string to false; update certificate_type to explicitly
accept only "true" or "false" (e.g., use z.enum(["true","false"]) or
z.string().refine(v => v === "true" || v === "false")) and then transform to a
boolean (map "true" -> true, "false" -> false) so invalid values fail
validation; locate certificateSchema and the certificate_type entry to make this
change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: c624f8d7-5db6-4ec7-9c3f-cbb2b0b80c5e
📒 Files selected for processing (4)
src/app/candidate/edit/page.tsxsrc/modules/candidates/WorkLogStaffActions.tsxsrc/modules/candidates/actions.tssrc/modules/workspace/data.ts
✅ Files skipped from review due to trivial changes (1)
- src/modules/candidates/WorkLogStaffActions.tsx
| const certificateSchema = z.object({ | ||
| certificate_type: z.string().transform((v) => v === "true").pipe(z.boolean()), |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Zod 3.x, does z.string().transform(v => v === "true").pipe(z.boolean())accept arbitrary strings like "foo" and parse them asfalse instead of failing validation?
💡 Result:
Yes. In Zod 3.x, your schema will not fail on arbitrary strings; it will transform them to a boolean based on your transform function. Given: z.string().transform(v => v === "true").pipe(z.boolean()) - z.string() accepts any string input. - .transform(...) runs for every accepted string and returns a boolean value: it returns true only when the input string is exactly "true", and returns false for any other string (including "foo"). - .pipe(z.boolean()) then validates the already-transformed value with z.boolean(). Since the transform always outputs a real boolean, validation always succeeds. So for input "foo", the transform yields false, and the final parse succeeds as false (not a validation error). This is consistent with Zod’s general behavior: transforms accept anything and perform a unidirectional transformation, and they’re commonly used with pipes to validate the transformed result rather than re-validate the original input [1]. If you instead want Zod to fail on strings other than "true"/"false", you must implement that in the transform (e.g., throw/add an issue for other values) or use a literal/enum-based schema rather than a boolean-valued equality transform [2].
Citations:
Reject non-"true"/"false" certificate_type values (prevent silent coercion)
File: src/modules/candidates/actions.ts (lines 498-499)
z.string().transform((v) => v === "true").pipe(z.boolean()) accepts any string; it transforms unknown values (e.g., "foo") to false, so validation never fails.
Suggested fix
const certificateSchema = z.object({
- certificate_type: z.string().transform((v) => v === "true").pipe(z.boolean()),
+ certificate_type: z.enum(["true", "false"]).transform((v) => v === "true"),
start_date: z.string().max(10).optional(),
end_date: z.string().max(10).optional(),
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const certificateSchema = z.object({ | |
| certificate_type: z.string().transform((v) => v === "true").pipe(z.boolean()), | |
| const certificateSchema = z.object({ | |
| certificate_type: z.enum(["true", "false"]).transform((v) => v === "true"), | |
| start_date: z.string().max(10).optional(), | |
| end_date: z.string().max(10).optional(), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/modules/candidates/actions.ts` around lines 498 - 499, The current
certificateSchema's certificate_type uses z.string().transform((v) => v ===
"true").pipe(z.boolean()) which silently coerces any non-"true" string to false;
update certificate_type to explicitly accept only "true" or "false" (e.g., use
z.enum(["true","false"]) or z.string().refine(v => v === "true" || v ===
"false")) and then transform to a boolean (map "true" -> true, "false" -> false)
so invalid values fail validation; locate certificateSchema and the
certificate_type entry to make this change.
Summary
rejection_reasonfield tocandidate_id_requestPrisma schemaapproveIdRequestandrejectIdRequestserver actions with Zod validation (rejection reason: min 10, max 500 chars)requireRoleCapability("inspector", "id_review.mutate")IdRequestActionsclient component with approve/reject UITest plan
npx tsc --noEmit)Summary by CodeRabbit
Release Notes
New Features
Style
Improvements