#548 Applicant Emails And Reviewer Warnings - #622
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
6 open — 1 🔴 Critical, 5 🟡 Low (see inline)
3b62057 to
f744c1c
Compare
Revision — Cycle 1fixed R1-C1, R1-L1, R1-L2, R1-L3, R1-L4, R1-L5 · f744c1c note: R1-C1's literal suggested fix ( |
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 2 · approved
0 open — clean
Revision feedback — real bugs + scope changes1. 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 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 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). |
f744c1c to
5f1ea80
Compare
Revision — human feedback follow-upAddressed all four points from the review feedback comment above · 5f1ea80
Docs updated: |
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 3 · needs revision
3 open — 1 🔴 Critical, 2 🟡 Low (see inline)
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>
5f1ea80 to
218a28d
Compare
Revision — Cycle 3fixed R3-C1, R3-L1, R3-L2 · 218a28d |
b-at-neu
left a comment
There was a problem hiding this comment.
Code Review — Cycle 4 · approved
0 open — clean
Closes #548
Summary
lib/email/application-emails.ts) that runs fromafter()so a provider failure can never fail the mutation it follows.scheduledAt; any further status change to that application inside the window cancels the pending send.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.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), anddocs/PERMISSIONS.md(the bulk row's new consequence, no new gate).Changes
lib/email/templates.ts—applicationReceivedEmail,applicationAcceptedEmail,applicationRejectedEmail,APPLICANT_EMAIL_FOOTER.lib/email/resend.ts—SendEmailParamsgainsapplicationId/scheduledAt; newsendEmailBatch(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.ts—DECISION_EMAIL_DELAY_MINUTES,DECISION_EMAIL_NOUNS,DECISION_EMAIL_TEMPLATES,RESEND_BATCH_MAX_EMAILS.lib/utils.ts—getFirstName,getDecisionEmailWarning,getBulkImmediateEmailWarning,getUndoDecisionEmailNotice,countBulkEmailRecipients.lib/types.ts—DecisionEmailNoticeState.prisma/data/applications.ts—getDecisionEmailNotice.prisma/actions/applications.ts—submitApplication,updateApplicationStatus,updateApplicationStatuseseach dispatch their email inafter(); 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 newgetFirstNameinstead of its inline split.tests/stubs/next-server.ts(new) +vitest.config.ts—after()alias for thedbtest 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
EmailLoghas oneapplication_receivedrow withapplicationIdanduserIdset.EmailLoghas oneapplication_acceptedrow,scheduled, withproviderMessageIdandscheduledAt~15 minutes out.cancelledand nothing ever arrives.cancelled, a fresh rejection isscheduled, and only the rejection ever arrives.applied → reached_out → reviewing: no warning shown, noEmailLogrow written.sentrows appear, each with its ownproviderMessageIdandapplicationId.sentrows.reached_out: no callout appears and the button label is unchanged.RESEND_FROM_EMAILto an unverified domain and accept: the status change still commits, the reviewer sees only the success toast, and onefailedrow carries the provider message.npm run prettier:check && npm run eslint:check && npm run tsc:check && npm run testall pass.Automated checks
npm run prettier:check— passnpm run eslint:check— passnpm run tsc:check— passnpm run test:unit— 263/263 pass (tests/db/**could not run in this sandbox: noDATABASE_URL/Docker access here; CI's Postgres service should run the fulldbproject, including the newtests/db/application-emails.test.ts)Notes
docs/PERMISSIONS.md).EmailLog, not derived from the status event's timestamp, so a bulk-sent decision never reads "hasn't been sent yet."sendApplicationReceipt,dispatchDecisionEmail,dispatchBulkDecisionEmails) swallows internally — a send failure never fails the mutation and is never surfaced to the reviewer.