Conversation
…blast The voter roll's per-voter "Email invite status" (email_data.inviteResponse, read by EnhancedTable's invite_status column and EditElectionRoll) was only written by the legacy sendInvites / sendInvite/:voter_id endpoints, which no current UI calls. Invitations actually go out through the email-blast endpoint (sendEmails), which never wrote the field - so after an admin sent invitations, every roll row still read "Invite not sent", and an admin checking mid-election saw failures that didn't happen. Fix: - Extract the legacy path's inviteResponse-recording logic into a shared recordInviteResponse() helper (sendInvitesController.ts) and call it from handleSendEmailEvent for each voter the blast actually targeted, writing the exact shape the legacy path writes so the frontend needs no change. - Only blasts started from the "invite" template update the status. The template choice already existed in SendEmailDialog but was never sent to the backend; it now rides along as an optional `template` field on email_request_data. A "blank" update/reminder blast deliberately does NOT flip "Not Sent" to "Sent" - that would be the reverse bug (reporting an invite that wasn't sent). - Test blasts (target: 'test') are unchanged: they already skip all roll updates. - The mock EmailService now returns SendGrid-shaped responses ([[ClientResponse, body], ...]) so tests can assert on statusCode the way production code reads it. - New backend test (sendEmailInviteStatus.test.ts): an invite-template blast marks every targeted roll as invited (statusCode < 400); a blank blast leaves inviteResponse unset. One of the bugs listed in Equal-Vote#1556. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Deploy Preview for bettervoting ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reached
Next review available in: 44 minutes Limit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?Wait for the limit to reset, then comment An organization admin can change what happens after included review limits in Billing. How do review limits work?CodeRabbit enforces per-developer PR review limits within each organization. For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Description
The voter roll's per-voter "Email invite status" can report failures that didn't happen. The field the admin UI reads is
email_data.inviteResponse— rendered by theinvite_statuscolumn inpackages/frontend/src/components/EnhancedTable.tsx(lines 102–118:undefined→ "Not Sent",inviteResponse[0].statusCode >= 400→ "Failed", else "Sent") and by the per-voter view inpackages/frontend/src/components/Election/Admin/EditElectionRoll.tsx(lines ~168–191).That field was only ever written by the legacy
sendInvites/sendInvite/:voter_idendpoints —sendInvitation()inpackages/backend/src/Controllers/Election/sendInvitesController.ts(previously lines 137–149) — which no current UI calls. Invitations actually go out through the email-blast endpoint (sendEmails, handled byhandleSendEmailEventinpackages/backend/src/Controllers/Election/sendEmailController.ts), which appended a history entry but never wroteemail_data.inviteResponse. So after an admin sent invitations via the blast tool, every roll row still read "Invite not sent" — an admin checking mid-election sees failures that didn't happen and may re-send to everyone (and the legacy endpoint's already-invited filter also couldn't see the blast, so a stray call to it would re-invite everyone too).The fix
Shared helper instead of a second write path. The legacy path's recording logic is extracted into
recordInviteResponse()(exported fromsendInvitesController.ts) and called fromhandleSendEmailEventfor each voter the blast actually targeted — writing the exact shape the legacy path writes, so the frontend status rendering needs no change.Only invitation-type blasts update the status.
SendEmailDialogalready makes the admin pick a template — "Invitation Template" or "Empty Template" — but the choice was only used to prefill the editable subject/body and was never sent to the backend. It now rides along as an optionaltemplate?: 'invite' | 'blank'field onemail_request_data(a type the frontend already imports from the backend controller, so it propagates with no duplication).Why gate on the template rather than marking every blast: the column is defined by both existing surfaces as "was this voter successfully sent their invitation" — the legacy path writes it only when sending the
Invitestemplate (the email carrying the voter's unique voting link), and the frontend renders it under the heading "Email Invites". Marking a generic "blank" update or a reminder-to-voters blast as an invite would introduce the reverse bug: reporting "Sent" for a voter who was never actually sent an invitation (the blank template's body is free-form and need not contain__VOTE_BUTTON__). Blasts sent before this change have notemplatefield and are treated the same asblank— no status change, which was the previous behavior.Test blasts unchanged:
target: 'test'already skips all roll updates and still does.The SendGrid
email_eventsledger (webhook-driven, shown byEmailEventsList) is untouched — the blast path already inserts itssentevent; this change only adds the roll-status write that was missing.The mocked
EmailService(packages/backend/src/Services/Email/__mocks__/EmailService.ts) now returns SendGrid-shaped responses ([[ClientResponse, body], …], matching howsendInvitesControllerreadsemailResponse[0][0].statusCode) so tests can assert on the recorded status. Nothing else readssendEmails's return value.Tests
New
packages/backend/src/test/sendEmailInviteStatus.test.ts, following the house supertest/TestHelper/MockEventQueue pattern:target: 'all'marks every targeted roll withinviteResponse[0].statusCode < 400(renders as "Sent").inviteResponseunset (still "Not Sent").Ran locally: full backend suite 25 suites / 177 tests passed (includes the 3 new tests);
npx tsc --noEmitclean in bothpackages/backendandpackages/frontend.Screenshots / Videos (frontend only)
The only frontend change is threading the already-displayed template choice into the request payload (
SendEmailDialog.tsx,ViewElectionRolls.tsx,EditElectionRoll.tsx) — no visual change. The status column itself renders exactly as before; it just becomes truthful after an invite blast.Related Issues
First bullet of the "Bugs noticed while writing" list in #1556:
🤖 Generated with Claude Code