Skip to content

#548 Applicant Emails And Reviewer Warnings - #622

Open
b-at-neu wants to merge 4 commits into
devfrom
548-applicant-emails-and-reviewer-warnings
Open

#548 Applicant Emails And Reviewer Warnings#622
b-at-neu wants to merge 4 commits into
devfrom
548-applicant-emails-and-reviewer-warnings

Conversation

@b-at-neu

Copy link
Copy Markdown
Collaborator

Closes #548

Summary

  • Adds the three applicant-facing email templates (submission receipt, accepted, rejected) and a dispatch layer (lib/email/application-emails.ts) that runs from after() so a provider failure can never fail the mutation it follows.
  • Single accept/reject (quick actions, override Select, and Undo) now schedules the decision email 15 minutes out via Resend's scheduledAt; any further status change to that application inside the window cancels the pending send.
  • Bulk accept/reject now sends immediately through Resend's batch endpoint (chunked at 100, permissive validation), with no window and no cancel step, since bulk eligibility is forward-only.
  • Every accept/reject surface now warns the reviewer before acting — the single-decision ConfirmDialog, the bulk bar's confirmation (a loud, distinct callout since bulk is irreversible), and the override dialog — plus an Undo notice that distinguishes a still-cancellable send from one already gone.
  • Updates docs/WORKFLOWS.md (new XC-8, plus AP-9/AP-13/AP-14/PM-11/PM-12/PM-14), docs/ENGINEERING.md §4 (the swallow-at-the-dispatch-boundary invariant), and docs/PERMISSIONS.md (the bulk row's new consequence, no new gate).

Changes

  • lib/email/templates.tsapplicationReceivedEmail, applicationAcceptedEmail, applicationRejectedEmail, APPLICANT_EMAIL_FOOTER.
  • lib/email/resend.tsSendEmailParams gains applicationId/scheduledAt; new sendEmailBatch (chunked, permissive, id-mapping guarded).
  • lib/email/application-emails.ts (new) — sendApplicationReceipt, dispatchDecisionEmail (cancel-then-schedule), dispatchBulkDecisionEmails, cancelPendingDecisionEmails; the only place that swallows.
  • lib/constants.tsDECISION_EMAIL_DELAY_MINUTES, DECISION_EMAIL_NOUNS, DECISION_EMAIL_TEMPLATES, RESEND_BATCH_MAX_EMAILS.
  • lib/utils.tsgetFirstName, getDecisionEmailWarning, getBulkImmediateEmailWarning, getUndoDecisionEmailNotice, countBulkEmailRecipients.
  • lib/types.tsDecisionEmailNoticeState.
  • prisma/data/applications.tsgetDecisionEmailNotice.
  • prisma/actions/applications.tssubmitApplication, updateApplicationStatus, updateApplicationStatuses each dispatch their email in after(); no change to their signatures, scoping, or error shapes.
  • components/features/use-application-status-move.ts, applications-bulk-bar.tsx, application-status-dialog.tsx, application-status-header-actions.tsx — the warnings and undo notice.
  • app/(main)/(auth)/applications/[id]/page.tsx — fetches the notice state.
  • components/features/user-dashboard.tsx — uses the new getFirstName instead of its inline split.
  • tests/stubs/next-server.ts (new) + vitest.config.tsafter() alias for the db test project (fire-and-forget by default).
  • tests/unit/email-templates.test.ts, tests/unit/utils.test.ts (extended), tests/db/application-emails.test.ts (new).
  • docs/WORKFLOWS.md, docs/ENGINEERING.md, docs/PERMISSIONS.md.

No Prisma migration — #547 already shipped every column and enum member this ticket reads (applicationId, scheduledAt, providerMessageId, EmailStatus.scheduled/cancelled, EmailTemplateKey.application_received/application_accepted/application_rejected).

Testing plan

  • Submit an application: the receipt arrives with the position title and a working View my application button; EmailLog has one application_received row with applicationId and userId set.
  • Withdraw, edit, resubmit: a second receipt arrives.
  • Accept a single application: the confirm dialog warns the applicant will be emailed in 15 minutes; no mail arrives immediately; EmailLog has one application_accepted row, scheduled, with providerMessageId and scheduledAt ~15 minutes out.
  • Reopen the status dialog: the undo notice reads "hasn't been sent yet — undoing this cancels it." Click Undo — the row flips to cancelled and nothing ever arrives.
  • Accept again, wait out the window, then undo: the notice reads "has already been sent," the mail is in the inbox, and the cancel attempt leaves the row alone (no toast, no error shown).
  • Accept, then override straight to Rejected inside the window: the acceptance is cancelled, a fresh rejection is scheduled, and only the rejection ever arrives.
  • Move an application applied → reached_out → reviewing: no warning shown, no EmailLog row written.
  • As an admin, bulk-reject ~3 applications: the confirmation shows the immediate-send callout and a confirm button reading "Reject 3 and email now"; all three emails arrive within seconds; three sent rows appear, each with its own providerMessageId and applicationId.
  • Open one of those applications: the undo notice reads "has already been sent," not "hasn't been sent yet."
  • As a manager (non-admin, managing the position), bulk-reject ~3 applications: identical behaviour to the admin run — same callout, same label, mail arrives, three sent rows.
  • Bulk-move to reached_out: no callout appears and the button label is unchanged.
  • Check the rejection email in a mail client: the subject line does not reveal the outcome, and the footer explains why it was received.
  • Set RESEND_FROM_EMAIL to an unverified domain and accept: the status change still commits, the reviewer sees only the success toast, and one failed row carries the provider message.
  • Screen reader: changing the status Select to Accepted announces the warning; the undo notice is announced with its button; the bulk callout text is read as part of the dialog description.
  • npm run prettier:check && npm run eslint:check && npm run tsc:check && npm run test all pass.

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — 263/263 pass (tests/db/** could not run in this sandbox: no DATABASE_URL/Docker access here; CI's Postgres service should run the full db project, including the new tests/db/application-emails.test.ts)

Notes

  • No migration in this ticket — Email Foundation And Send Log #547 shipped every column/enum member it reads.
  • No email-specific permission gate on the bulk path, by design: whoever may run a bulk accept/reject already sends its emails (documented in docs/PERMISSIONS.md).
  • The undo notice is read from EmailLog, not derived from the status event's timestamp, so a bulk-sent decision never reads "hasn't been sent yet."
  • Every domain email dispatch (sendApplicationReceipt, dispatchDecisionEmail, dispatchBulkDecisionEmails) swallows internally — a send failure never fails the mutation and is never surfaced to the reviewer.

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

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 28, 2026 1:24pm

@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 Aug 28, 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 · needs revision

6 open — 1 🔴 Critical, 5 🟡 Low (see inline)

Comment thread lib/email/application-emails.ts Outdated
Comment thread lib/email/application-emails.ts Outdated
Comment thread lib/types.ts Outdated
Comment thread tests/stubs/next-server.ts Outdated
Comment thread lib/email/resend.ts Outdated
Comment thread prisma/data/applications.ts Outdated
@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 Aug 28, 2026
@b-at-neu
b-at-neu force-pushed the 548-applicant-emails-and-reviewer-warnings branch from 3b62057 to f744c1c Compare August 28, 2026 12:18
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 1

fixed R1-C1, R1-L1, R1-L2, R1-L3, R1-L4, R1-L5 · f744c1c

note: R1-C1's literal suggested fix (resend.emails.cancel(row.providerMessageId) with no assertion) fails tsc — the Prisma select doesn't narrow on the where filter, so providerMessageId stays string | null. Addressed the underlying "no assertion" intent instead with a real narrowing guard (if (!row.providerMessageId) continue) · rebase: tests/unit/utils.test.ts (dual independent additions — merged both sides' imports and describe blocks)

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 28, 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 Aug 28, 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 · approved

0 open — clean

@b-at-neu b-at-neu added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 28, 2026
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision feedback — real bugs + scope changes

1. Show the scheduled send time in the UI. Right now the warning just says "in 15 minutes" relative to when the dialog was opened — there's no persistent way to see the actual scheduled timestamp afterward. Surface the actual scheduledAt time somewhere visible (e.g. in the undo notice: "The acceptance email is scheduled to send at 3:42 PM — undo before then to cancel it.").

2. Undo must become impossible after the window passes or the email sends — not just explained. Currently the Undo control stays clickable and only the notice text changes. That's wrong: disable/hide the Undo action itself once either (a) 15 minutes have passed, or (b) the email has actually sent — whichever comes first. This applies to both Accepted and Rejected. The notice text change alone doesn't stop the click.

3. Real bug — cancel isn't actually canceling in Resend. Repro: Accept an application (schedules email) → Undo within the window (moves back to Reviewing) → Accept the same application again → now there are TWO scheduled emails in Resend for that application, not one. The cancel-then-schedule flow in dispatchDecisionEmail/cancelPendingDecisionEmails isn't successfully canceling the prior Resend schedule before creating the new one. Find and fix the root cause — likely the cancel API call is failing silently (remember: a cancel failure is supposed to be logged, not thrown, but it should still actually try and we need to know why it's not working) or the EmailLog row lookup for the pending schedule isn't matching correctly.

4. New rule: one decision email ever, per application, permanently. Once a decision email has actually been sent (not just scheduled) for an application — whether Accepted or Rejected — no future status change may ever schedule another decision email for that same application again, no matter how many times the status flips back and forth afterward (accept → reviewing → reject → reviewing → accept, etc.). The first sent email is final. This needs a check before dispatching any new decision email: has this application already had a decision email reach 'sent' status? If yes, skip dispatch entirely (status change itself still succeeds — this is only about the email).

@b-at-neu b-at-neu added needs revision Review found issues that need fixing revising Revise agent working (in-flight) and removed approved Review passed, ready to merge needs revision Review found issues that need fixing labels Aug 28, 2026
@b-at-neu
b-at-neu force-pushed the 548-applicant-emails-and-reviewer-warnings branch from f744c1c to 5f1ea80 Compare August 28, 2026 13:12
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — human feedback follow-up

Addressed all four points from the review feedback comment above · 5f1ea80

  • Undo notice now shows the real scheduledAt (<LocalTime>), not a dialog-open-time guess.
  • Undo control itself disables once the window passes or the email sends (client-side wall-clock check + server sent state), for both Accepted and Rejected — not just a copy change.
  • Root cause of the double-schedule: dispatchDecisionEmail always scheduled a fresh send even when the prior cancel couldn't be verified in Resend. cancelPendingDecisionEmails now reports success/failure and the caller skips scheduling a new send whenever a prior one might still be live.
  • Added a permanent one-decision-email-ever gate (applicationsWithDispatchedDecisionEmail) checked before every single and bulk dispatch; the status write still succeeds, only the email is suppressed.

Docs updated: docs/WORKFLOWS.md (XC-8, PM-14), docs/ENGINEERING.md (§2). New/updated tests in tests/db/application-emails.test.ts and tests/unit/utils.test.ts.

@b-at-neu b-at-neu added ready for review PR ready for review agent and removed revising Revise agent working (in-flight) labels Aug 28, 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 Aug 28, 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 3 · needs revision

3 open — 1 🔴 Critical, 2 🟡 Low (see inline)

Comment thread tests/db/application-emails.test.ts Outdated
Comment thread lib/email/application-emails.ts Outdated
Comment thread components/features/application-status-dialog.tsx Outdated
@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 Aug 28, 2026
b-at-neu and others added 4 commits August 28, 2026 09:21
Adds the three applicant-facing templates, a dispatch layer that runs
in after() so a provider failure never fails its mutation, a scheduled
15-minute window for single decisions (cancellable by any further
status change), and an immediate irreversible batch send for bulk
decisions with matching reviewer warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Undo could still click through past the 15-minute window or an already-sent
email, and re-accepting after an undo could leave a prior Resend schedule
live while a fresh one was created, double-emailing the applicant. The undo
notice now shows the real scheduledAt instead of a dialog-open-time guess,
the Undo control disables itself once the window or the send closes it, a
cancel that can't be verified blocks the next schedule instead of racing it,
and a decision email that has ever reached sent permanently blocks any later
one for that application.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix the double-cancel mock so the retry-cancel test matches the real
call count, and trim two over-length comments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@b-at-neu
b-at-neu force-pushed the 548-applicant-emails-and-reviewer-warnings branch from 5f1ea80 to 218a28d Compare August 28, 2026 13:23
@b-at-neu

Copy link
Copy Markdown
Collaborator Author

Revision — Cycle 3

fixed R3-C1, R3-L1, R3-L2 · 218a28d

@b-at-neu b-at-neu added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed revising Revise agent working (in-flight) ready for review PR ready for review agent labels Aug 28, 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 4 · approved

0 open — clean

@b-at-neu b-at-neu added approved Review passed, ready to merge needs human Pipeline escalation: 3 review cycles without convergence and removed reviewing Review agent working (in-flight) needs human Pipeline escalation: 3 review cycles without convergence labels Aug 28, 2026
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.

Applicant Emails And Reviewer Warnings

1 participant