#348 Exclude Soft-Deleted and Draft Positions From Application Queries and Counts - #410
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 1 · needs revision
2 open — 1 🔴 Critical, 1 ⚪ Nit
R1-C1 🔴 Critical — required Vercel deployment check is failing on this PR: https://vercel.com/sga-operational-affairs-projects/aplio/G3PhBwPL2ombQQnZLrNAMJEWpvqR. Fix: resolve the deploy failure before merge (lint/tsc/prettier all pass, so this is isolated to the Vercel build/deploy step).
R1-N1 ⚪ Nit — getApplicationForReview's doc comment (
aplio/prisma/data/applications.ts
Lines 95 to 97 in c3e7a19
updateApplicationStatus. Fix: extend it to mention the position exclusion for consistency.
Revision — Cycle 1fixed R1-N1 · bddc23f R1-C1 (Vercel deploy failure): investigated, not fixable by a code change in |
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 2 · needs revision
1 open — 1 🔴 Critical (see below)
R2-C1 🔴 Critical — required Vercel deployment check is still failing on this PR: https://vercel.com/sga-operational-affairs-projects/aplio/9FY3qwjKq7CUtYJzE53QZiM7Nfk7. Fix: resolve the deploy failure before merge (lint/tsc/prettier all pass; this remains isolated to the Vercel build/deploy step).
Pipeline EscalationRebase: Conflicted file: The ambiguous hunkInside Ours ( export async function getApplicationForReview(
id: string,
user: { id: string; isAdmin: boolean },
): Promise<ApplicationForReview | null> {
const application = await prisma.application.findFirst({
where: { id, ...buildBaseWhere(user) },
...
function buildBaseWhere(user: { id: string; isAdmin: boolean }) {
return user.isAdmin
? { deletedAt: null, status: { not: 'draft' as const } }
: {
deletedAt: null,
status: { not: 'draft' as const },
position: { managers: { some: { id: user.id } } },
};
}Theirs (PR commit export async function getApplicationForReview(
id: string,
user: { id: string; isAdmin: boolean },
): Promise<ApplicationForReview | null> {
const where = user.isAdmin
? { id, deletedAt: null, position: PUBLISHED_POSITION_WHERE }
: {
id,
deletedAt: null,
position: {
...PUBLISHED_POSITION_WHERE,
managers: { some: { id: user.id } },
},
};
const application = await prisma.application.findFirst({
where,
...Note the local Why this isn't safe to auto-resolve
But mechanically dropping the conflicting local Per the rebase conflict protocol, this is "both sides modified the same function body… accepting one side would drop the other's [implicit] logic" — resolving it either way (keep the PR's original local What's neededPlease advise (as a PR comment or by pushing a fixup) whether
Once decided, re-run the pipeline (label |
Feedback from humanResolves the rebase-conflict escalation above. Decision: adopt
|
rule (deletedAt means "does not exist"; draft positions are hidden on cross-position surfaces but stay visible on position-scoped ones) is stated once, then apply them to every application/user query that joins through Position. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
submitApplication now rejects submitting against a since-deleted position with the same copy createDraftApplication already uses. updateApplicationStatus/updateApplicationStatuses merge the published- position constraint into their existing authorization where (merged, not overwritten, so manager scoping is preserved) so a stale id can't mutate a hidden record. deletePosition/updatePosition revalidate the cross-position surfaces whose contents they now change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The seed previously produced only open positions, which is why the missing position exclusions went unnoticed. Add a draft position and a soft-deleted position, each with a submitted application, plus a draft application on the deleted position, so every affected surface is regression-tested going forward. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend getApplicationForReview's doc comment to name the draft/deleted-position exclusion, matching updateApplicationStatus's sibling comment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
bddc23f to
d18476c
Compare
Revision — Cycle 2R2-C1 self-resolved: the flagged Vercel deploy ( |
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 3 · approved
0 open — clean
Closes #348
Summary
Positionnow excludes soft-deleted positions (always) anddraftpositions (on cross-position surfaces only) via two shared where-fragments inlib/constants.ts.submitApplicationgains a deleted-position gate;updateApplicationStatus/updateApplicationStatusesfold the same exclusion into their authorizationwhere(merged, not overwritten, so manager scoping is unaffected).Changes
lib/constants.ts— addVISIBLE_POSITION_WHERE({ deletedAt: null }) andPUBLISHED_POSITION_WHERE({ deletedAt: null, status: { not: 'draft' } }), the rule stated once.prisma/data/applications.ts— applyPUBLISHED_POSITION_WHEREto every cross-position query (getMyApplications,getRecentMyApplications,getMyApplicationStatusCounts,getMySubmittedCount,getMyRecentActivity,getApplicationStatusCounts,getRecentApplications,buildBaseWhere's admin/manager branches — merged with the manager scope, not overwritten — andgetApplicationForReview's admin/manager branches); applyVISIBLE_POSITION_WHEREto the position-scopedgetPositionApplications/getPositionApplicationStats(draft stays visible there — the deliberate escape hatch);getReviewablePositionsexcludes draft too.prisma/data/users.ts+lib/types.ts— the admin user list's_count.applications(and its mirroredAdminUserListItempayload type) now excludes hidden-position applications.prisma/actions/applications.ts—submitApplicationreturns{ error: 'This position is no longer available.' }when the position was soft-deleted after the draft was created (same copy ascreateDraftApplication);updateApplicationStatus/updateApplicationStatusesmerge the published-position constraint into their existing authorizationwhere.prisma/actions/position-actions.ts—deletePosition/updatePositionnow also revalidate/,/my-applications, and/applicationsso hidden rows and stale counts clear immediately.prisma/seed/types.ts,prisma/seed/positions.ts,prisma/seed/applications.ts,prisma/seed.ts—PositionDefgains optionalstatus/deleted; two new fixture positions (onedraft, one soft-deleted) each get a submitted application, plus a draft application on the deleted position.The recorded decision (per the plan): cross-position surfaces hide soft-deleted-or-draft-position applications entirely —
deletedAtmeans "does not exist" everywhere else in this codebase, and a surviving row is a dead link inflating reviewer counts. Position-scoped surfaces (getPositionApplications/stats) keep draft positions visible, since a manager reverting a position to draft should still see its applications on the position card.Testing plan
npm run db:reset, thennpm run prisma:seed) so the new draft and soft-deleted fixtures exist./my-applicationsshows neither row (no dead "Continue" button); the dashboard "My Applications" widget, its status summary line, the "Submitted" stat card, and the activity feed all exclude them./applicationslists neither row, the toolbar count matches the visible rows, and the position filter dropdown offers neither the draft nor the deleted position./dashboard pipeline totals and "Recent applications" exclude both;/usersper-user application counts drop by the hidden applications./applications/<hidden application id>→ 404, as both admin and as the position's manager./positions: the draft position's card still shows its application stats (escape hatch); the deleted position does not appear at all./applicationsstill shows only applications for positions you manage — confirm the mergedpositionwhere didn't widen the result set.openin/positions/<id>/edit→ its applications reappear across/my-applications,/applications, and the dashboards without a manual refresh; flip it back todraft→ they disappear again./my-applicationsand/applicationsimmediately (revalidation), and submitting a draft application against it returns "This position is no longer available."Automated checks
npm run prettier:checknpm run eslint:checknpm run tsc:checkNotes
positionmerge inbuildBaseWhere,getApplicationForReview, and both status-update actions was the one real hazard flagged in the plan — overwriting instead of merging would have silently widened a manager's visibility to every position. Verified each branch spreadsPUBLISHED_POSITION_WHEREalongside the existingmanagers: { some: { id: user.id } } }constraint rather than replacing it.deleteDraftApplicationno longer reachable) but stays in the database as an orphan row — acceptable and consistent with what soft-deleting the position means; restoring the position resurfaces it via the existing@@unique([userId, positionId])constraint.withdrawApplication,reopenApplication, anddeleteDraftApplicationwere deliberately not touched — they're self-service on the caller's own record and unreachable from the UI once hidden; gating them would only produce confusing errors on rows the user can no longer see.getPositionApplications/PositionApplicationsTable/PositionApplicationListItemcurrently have no consumer. They're constrained here for consistency rather than deleted; removing the dead triple is a separate follow-up..envwas available in this worktree (by design), so the DB-dependent testing steps above are left as checklist items for review rather than something I ran myself;npm run prettier:check/eslint:check/tsc:checkall pass locally.