Skip to content

#741 Let Admins Force-Withdraw An Application Without Notifying - #755

Merged
b-at-neu merged 7 commits into
devfrom
741-force-withdraw-application
Sep 25, 2026
Merged

b-at-neu merged 7 commits into
devfrom
741-force-withdraw-application

Conversation

@b-at-neu

Copy link
Copy Markdown
Collaborator

Closes #741

Summary

  • Adds an admin-only forceWithdrawApplication server action that moves an application straight to withdrawn from any submitted status — including accepted/rejected — without ever sending an email.
  • Wires a destructive Force withdraw entry into the status override dialog and the shared status menu (caret dropdown / table row ⋯), admin-only, hidden for draft/withdrawn, behind a ConfirmDialog that states plainly the applicant is not notified and can edit-and-resubmit.
  • Updates docs/PERMISSIONS.md and docs/WORKFLOWS.md in the same PR, since this deliberately reopens the withdraw → resubmit round-trip that withdrawApplication blocks for the applicant's own action — the docs now describe that hole as an explicit, admin-only, audited product decision rather than leaving them asserting a guarantee the code no longer makes.

Changes

  • prisma/actions/applications.ts — new forceWithdrawApplication: requireAdmin() → zod → $transaction (scoped findFirst → status eligibility → CAS updateMany → applicationStatusEvent.create) → the same two revalidatePath calls updateApplicationStatus makes. No after() dispatch, ever. Also extracts updateApplicationStatus's inline write-race sentence into the new shared APPLICATION_STATUS_CHANGED_MESSAGE constant (byte-identical).
  • lib/constants.ts — APPLICATION_STATUS_CHANGED_MESSAGE and APPLICATION_DRAFT_NOT_WITHDRAWABLE_MESSAGE.
  • lib/icons.ts — registers forceWithdraw: CircleSlash in ACTION_ICONS.
  • components/features/use-force-withdraw-application.ts — new client hook mirroring use-application-status-move.ts's shape (pending state, action call, toasts, confirmDialogProps).
  • components/features/application-status-dialog.tsx — isAdmin prop; destructive Force withdraw block below Change status, gated the same way as that block (!isNonReviewableApplicationStatus); disables the Select/Apply while either write is pending.
  • components/features/application-status-menu.tsx — canForceWithdraw/onForceWithdraw props; destructive item below a separator, last in the menu.
  • components/features/application-status-header-actions.tsx, application-status-actions.tsx — isAdmin prop; each owns its own useForceWithdrawApplication + ConfirmDialog for the menu entry point, and forwards isAdmin to ApplicationStatusDialog. The table row's two early returns (non-reviewable / terminal-decision) are untouched.
  • components/features/applications-table.tsx, applications-results.tsx, app/(main)/(auth)/manage/applications/[id]/page.tsx — thread isAdmin={user.isAdmin} down from the server.
  • docs/PERMISSIONS.md — new server-action row; lifecycle table gains "admin only: force withdraw" on the unresolved/accepted/rejected rows; amends the terminal-withdrawal bullet to scope the block to the applicant's own withdrawApplication only.
  • docs/WORKFLOWS.md — new AD-13 Force-withdraw an application + TOC entry; amends AP-13, AP-14, PM-14, and XC-9.
  • tests/db/application-transitions.test.ts — new forceWithdrawApplication suite.

Testing plan

  • As an admin, open /manage/applications/[id] for an applied application → caret → Force withdraw → confirm. Toast says force-withdrawn and names no email; status badge reads Withdrawn; See more shows a new history row Applied → Withdrawn · <your name>.
  • Repeat from an accepted application via Change decision → dialog → Force withdraw. The confirm text includes the "already been told they were Accepted" sentence.
  • Repeat from a rejected application, and from reached_out / interview_scheduled / reviewing.
  • /emails and the application's Email history — no new row for any of the above.
  • Sign in as the affected applicant: /applications shows Withdrawn with Edit & resubmit, and no email arrived. Resubmit → back in the queue as Applied.
  • As a manager on the same position: no Force withdraw in the caret, the row ⋯, or the dialog, at any status.
  • As an admin on a draft and on an already-withdrawn application: no Force withdraw anywhere.
  • Two tabs: force-withdraw in tab A, then confirm the same action in tab B → error toast, nothing written twice.
  • /manage/applications row ⋯ on an applied row → Force withdraw works from the table too; the row's menu then disappears (expected until Show The See More Caret On Every Application Status #742 restores the terminal-decision caret).
  • 375px viewport: the dialog block wraps, the confirm dialog is readable, touch targets stay ≥44px.
  • Keyboard only: reach the menu item, open the confirm, Escape cancels before submit and is blocked during.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test (unit + db, against a local Postgres) — 783/783 pass, including the new forceWithdrawApplication suite (per-status matrix, manager/applicant rejection, no EmailLog row, concurrent-call CAS race)

Notes

  • This is a deliberate, product-approved hole in the "withdrawal is blocked from a terminal status" guarantee, scoped to admins only: an admin can put a rejected/accepted applicant back into an editable, resubmittable state. The decision survives permanently in the append-only ApplicationStatusEvent history even though it no longer shows in Application.status.
  • Per the plan's scope boundary vs Show The See More Caret On Every Application Status #742: application-status-actions.tsx's two early returns (non-reviewable / terminal-decision rows get no table-row ⋯ at all) are untouched here. Until Show The See More Caret On Every Application Status #742 restores that caret, accepted/rejected reach Force withdraw only through the detail page (Change decision → dialog), which still satisfies "available from any submitted status."

@b-at-neu b-at-neu added the claude Will be worked on by Claude label Sep 21, 2026
@b-at-neu b-at-neu self-assigned this Sep 21, 2026
@vercel

vercel Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
aplio Ready Ready Preview Sep 25, 2026 2:29pm UTC

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 21, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 1 · approved

0 open — no findings

@b-at-neu b-at-neu added approved Review passed, ready to merge needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) approved Review passed, ready to merge needs revision Review found issues that need fixing labels Sep 21, 2026
@b-at-neu
b-at-neu force-pushed the 741-force-withdraw-application branch from b9260ef to c1090f5 Compare September 23, 2026 17:41
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

rebase-only: resolved conflict with #742's caret restructuring · c1090f5
rebase: components/features/application-status-header-actions.tsx (merged — kept dev's extracted confirmDialog var, added the new force-withdraw ConfirmDialog)
rebase: docs/WORKFLOWS.md (merged — kept dev's PM-14 trigger text for the consolidated caret, kept this PR's Force withdraw happy-path addition)

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 23, 2026
@b-at-neu b-at-neu added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 23, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 2 · needs revision

1 open — 1 🟡 Low (see below)

R2-L1 🟡 Low — Vercel preview deployment failed while run-neon-check is green, so branch-quota exhaustion doesn't explain it. This diff has no schema/migration change and run-prettier-check/run-linting-check/run-tsc-check/run-build-check/run-test-check all pass, so nothing in the changed surface plausibly explains the failure either — worth checking the deploy directly before merging rather than assuming it's noise. https://vercel.com/sga-operational-affairs-projects/aplio/BiTyTdimSfE61fSF2cuhtuMVr6Wy

@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 23, 2026
@b-at-neu
b-at-neu force-pushed the 741-force-withdraw-application branch from c1090f5 to cfabf5a Compare September 23, 2026 23:56
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 2

fixed R2-L1 (root cause: branch was 5 merges stale vs dev, missing 747's submittedAt-nullable migration that vercel.json's preview buildCommand applies via prisma migrate deploy against the shared-lineage preview DB, which CI's run-build-check never runs against; rebase onto current dev restored migration-folder parity) · cfabf5a

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 23, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 5 · needs revision

1 open — 1 🟠 Medium (see inline)

Comment thread docs/WORKFLOWS.md Outdated
@b-at-neu b-at-neu added needs revision Review found issues that need fixing needs human Pipeline escalation: 3 review cycles without convergence and removed reviewing Review agent working (in-flight) needs revision Review found issues that need fixing labels Sep 25, 2026
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Pipeline Escalation

This PR has hit the 5-review cycle cap with the latest cycle still producing a Medium finding, so the pipeline is escalating for human attention rather than dispatching another automated revision.

Cycle 5 finding (R5-M1, Medium): docs/WORKFLOWS.md's AD-13 "Trigger" bullet says direct caret/table-row access to Force withdraw is scoped to "the four unresolved statuses." This is stale relative to the shipped code — application-status-header-actions.tsx and application-status-actions.tsx both thread canForceWithdraw={isAdmin && !isNonReviewableApplicationStatus(currentStatus)} unconditionally, so accepted/rejected also get Force withdraw directly in the caret/table ⋯ menu (matching docs/PERMISSIONS.md's lifecycle table), not only via the Change decision dialog as AD-13 currently claims.

Context on why this cycled so many times: most of the prior cycles (2–4) were rebase-only dispatches responding to dev outpacing this branch, not new findings — the actual code has been stable since cycle 3's approval. This Medium is a one-line documentation wording fix (correcting "four unresolved statuses" scoping in AD-13 to match the already-correct code and the already-correct PERMISSIONS.md table).

Recommendation: this looks safe to fix directly (either by a human editing the one line, or by approving one more revise-agent dispatch outside the automated cap) — it is not a design disagreement, just a stale doc sentence.

@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed needs human Pipeline escalation: 3 review cycles without convergence needs revision Review found issues that need fixing labels Sep 25, 2026
b-at-neu and others added 7 commits September 25, 2026 10:27
Admin-only path back to withdrawn from any submitted status, including
accepted/rejected. Copies updateApplicationStatus's CAS-transaction
shape but skips the after() email dispatch entirely, so that action's
"everything here emails" invariant stays unconditional.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useForceWithdrawApplication mirrors use-application-status-move's
shape (pending state, action call, toasts, confirmDialogProps). The
status dialog gets a destructive Force withdraw block, admin-only and
hidden for draft/withdrawn; the shared status menu gets a matching
destructive item below a separator. Both table row and header-actions
hosts thread isAdmin down and own their own confirm dialog for the
menu entry point, separate from the dialog's own instance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per-status matrix over all eight statuses, a manager and the owning
applicant both rejected, no EmailLog row on success, and a concurrent
double-call resolving to exactly one success and one new event.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PERMISSIONS.md gets the new action's authorization row, the lifecycle
table's admin-only force-withdraw note on the unresolved/accepted/
rejected rows, and an amendment to the terminal-withdrawal bullet
clarifying the block is applicant-scoped only. WORKFLOWS.md adds
AD-13 and touches AP-13, AP-14, PM-14, and XC-9 so none of them still
assert a guarantee this ticket deliberately punches a hole in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rebases past #742's consolidated-caret restructuring; merges the
extracted confirmDialog variable with the new force-withdraw confirm
dialog, and reconciles the WORKFLOWS.md PM-14 trigger/happy-path text
with the force-withdraw addition. Also threads the now-required
isAdmin prop through the two draft-status ApplicationStatusActions
call sites the rebase didn't touch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The note!==null caret branch in application-status-header-actions.tsx
rendered ApplicationStatusMenu without canForceWithdraw/onForceWithdraw,
so admins on the detail page couldn't force-withdraw accepted/rejected
applications from the caret (only via Change decision dialog), unlike
the table row's identical menu.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The "four unresolved statuses" scoping was stale post-#742; the shipped
canForceWithdraw check allows any status except draft/withdrawn.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@b-at-neu
b-at-neu force-pushed the 741-force-withdraw-application branch from b6ead73 to 16770bb Compare September 25, 2026 14:28
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 5

fixed R5-M1 · 16770bb

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Sep 25, 2026
@b-at-neu b-at-neu added reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Sep 25, 2026

@b-at-neu b-at-neu left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Cycle 6 · approved

0 open — no findings

@b-at-neu b-at-neu added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Sep 25, 2026
@b-at-neu
b-at-neu merged commit c737d89 into dev Sep 25, 2026
14 of 19 checks passed
@b-at-neu
b-at-neu deleted the 741-force-withdraw-application branch September 25, 2026 14:34

This branch was successfully deployed

1 active deployment
Preview — 16770bbf Deployed Sep 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Let Admins Force-Withdraw An Application Without Notifying

1 participant