Hacker admissions emails and Tito duplicate check fix#516
Open
Hacker admissions emails and Tito duplicate check fix#516
Conversation
When a pre-fetched existingInvitationsMap is provided (email→url): - If the email is already in the map, return the cached URL immediately without hitting the Tito API at all. - Otherwise, create normally.
There was a problem hiding this comment.
Pull request overview
Adds hacker admissions emailing (single + bulk) into the admin invites workflow while also reducing Tito duplicate-check latency during bulk sends.
Changes:
- Introduces hacker admission invite types (accept/waitlist_accept/waitlist/reject) and corresponding single/bulk invite responses.
- Consolidates the admin invites UI into a unified
InvitePanel(single + bulk) shared across hackers/judges/mentors/volunteers. - Optimizes Tito duplicate handling by prefetching existing RSVP invitations into an email→URL map and reusing URLs when possible.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| app/_types/emails.ts | Adds hacker admission types, labels, and response/result types used across UI + server actions. |
| app/(pages)/admin/invites/page.tsx | Updates invites page to include Hackers tab and use the consolidated InvitePanel. |
| app/(pages)/admin/_components/InvitePanel/InvitePanel.tsx | New shared panel for single/bulk invite flows across roles (with Tito config fetching). |
| app/(pages)/admin/_components/InvitePanel/SingleInviteForm.tsx | New unified single-invite form including hacker admission decision handling. |
| app/(pages)/admin/_components/InvitePanel/BulkInviteForm.tsx | New unified bulk-invite form including hacker CSV Type column + result downloads. |
| app/(pages)/admin/_utils/generateInviteResultsCSV.ts | Adds optional admission Type column support for results export. |
| app/(pages)/admin/_utils/generateInviteFailuresCSV.ts | Adds optional Type column in failures export to enable re-upload for retries. |
| app/(api)/_actions/emails/sendSingleHackerInvite.ts | Implements server action for single hacker admissions emails (email-only vs Tito+Hub). |
| app/(api)/_actions/emails/sendBulkHackerInvites.ts | Implements server action for bulk hacker admissions emails using custom CSV parser. |
| app/(api)/_actions/emails/parseHackerAdmissionsCSV.ts | Adds CSV parsing/validation for hacker admissions (Type column). |
| app/(api)/_actions/emails/processBulkInvites.ts | Allows swapping the default CSV parser with a custom parser per bulk flow. |
| app/(api)/api/admissions/hackers/route.ts | Adds an authenticated admissions API endpoint for batch sending hacker emails. |
| app/(api)/_actions/tito/getAllRsvpInvitations.ts | New helper to prefetch all RSVP invitations for duplicate short-circuiting. |
| app/(api)/_actions/tito/getOrCreateTitoInvitation.ts | Extends Tito invite creation to accept a prefetched invitation map for faster duplicate handling. |
| app/(api)/_actions/emails/sendBulkMentorOrVolunteerInvites.ts | Uses prefetched Tito invitations map to reduce duplicate-check latency. |
| app/(api)/_actions/emails/transporter.ts | Makes Nodemailer transporter lazily initialized and exports DEFAULT_SENDER for guards. |
| app/(api)/_actions/emails/emailTemplates/2026HackerInviteTemplate.ts | Adds 2026 hacker acceptance email template. |
| app/(api)/_actions/emails/emailTemplates/2026HackerWaitlistAcceptTemplate.ts | Adds 2026 hacker waitlist-accept template. |
| app/(api)/_actions/emails/emailTemplates/2026HackerWaitlistTemplate.ts | Adds 2026 hacker waitlist template. |
| app/(api)/_actions/emails/emailTemplates/2026HackerRejectionTemplate.ts | Adds 2026 hacker rejection template. |
| Deleted legacy judge/mentor/volunteer invite form components | Removes now-redundant per-role invite forms replaced by InvitePanel. |
Comments suppressed due to low confidence (2)
app/(pages)/admin/_components/InvitePanel/InvitePanel.tsx:59
- When
rolechanges to a Tito-required role, the effect starts fetching RSVP lists/releases butloadingisn't set back totrue. This can renderSingleInviteForm/BulkInviteFormwith emptyrsvpLists/releasesbriefly and allow interaction before data is loaded. Consider settingsetLoading(true)at the start of the effect (whenneedsTito(role)), and optionally clearingloadError/data when switching roles.
app/(api)/_actions/tito/getOrCreateTitoInvitation.ts:56 - The comment says duplicate recovery is only for the single-invite path (no pre-fetched map), but the recovery logic runs even when
existingInvitationsMapis provided (it triggers on any duplicate error). Either update the comment to match the behavior, or gate the recovery behind!existingInvitationsMapif the intent is to avoid per-email lookups in bulk sends.
// Duplicate recovery for single-invite path (no pre-fetched map)
if (!titoResponse.ok && isDuplicateTicketError(titoResponse.error)) {
console.warn(`[Tito] Duplicate detected for ${email}, attempting recovery`);
const existingRes = await getRsvpInvitationByEmail(rsvpListSlug, email);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+49
to
+51
| // Pre-fetch all existing Tito invitations for rows that need them | ||
| const existingInvitationsMap = await getAllRsvpInvitations(rsvpListSlug); | ||
|
|
Comment on lines
+11
to
+24
| export default async function getAllRsvpInvitations( | ||
| rsvpListSlug: string | ||
| ): Promise<Map<string, string>> { | ||
| const map = new Map<string, string>(); | ||
| const pageSize = 1000; | ||
| let page = 1; | ||
|
|
||
| let hasMore = true; | ||
| while (hasMore) { | ||
| try { | ||
| const data = await TitoRequest<{ | ||
| release_invitations: ReleaseInvitation[]; | ||
| }>( | ||
| `/rsvp_lists/${rsvpListSlug}/release_invitations?page[size]=${pageSize}&page[number]=${page}` |
|
|
||
| const TAB_DESCRIPTIONS: Record<Tab, string> = { | ||
| hackers: | ||
| 'Send a Tito e-ticket and HackDavis Hub registration invite to hackers.', |
Comment on lines
+18
to
+20
| judges: | ||
| 'Send HackDavis Hub invites to judges. Navigate to emergency-invites for one-time links.', | ||
| mentors: 'Send Tito e-ticket invites to mentors.', |
Comment on lines
18
to
22
| /** | ||
| * Generates a CSV string from bulk invite results. | ||
| * @param rows Merged invite result rows (one per person). | ||
| * @param includeHub Set true for hacker invites that include a Hub URL column. | ||
| * @param rows Merged invite result rows (one per person). | ||
| * @param includeHub Set true for hacker invites that include a Hub URL column. | ||
| */ |
| email: string; | ||
| admissionType?: HackerAdmissionType; | ||
| titoUrl?: string; | ||
| hubUrl?: string; // populated for hacker invites; omitted for mentor-only |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #515, #502