confirm and adminConfirm now create REQUESTED VisaApplication rows for
every attendee with needsVisa=true, in the same Prisma transaction as
the AttendingMember inserts. editAttendees was extended into a fully
diff-aware sync: existing attendees whose needsVisa flips on get a new
VisaApp; flipping off deletes it; staying true preserves the row (and
its status / notes / dates). Removed attendees cascade automatically
via the FK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This edition is being handled manually via email — admins need to
record what each finalist replied. Adds:
- finalist.adminConfirm — flips PENDING → CONFIRMED with attendees +
visa flags. Same cap and team-membership checks as the public flow,
audit-logged as FINALIST_ADMIN_CONFIRM.
- finalist.adminDecline — flips PENDING → DECLINED with optional
reason and triggers waitlist promotion. Audit-logged as
FINALIST_ADMIN_DECLINE.
- finalist.getConfirmationDetail — feeds the admin attendee picker.
- Per-row Confirm / Decline actions on the Logistics > Confirmations
table (PENDING rows only) backed by a shared dialog that switches
between attendee-picker and reason-input modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Team-lead-only mutation that replaces the AttendingMember roster on a
CONFIRMED finalist confirmation. Diffs the requested user list against
existing rows: kept rows are updated in place (preserving FlightDetail),
removed rows are deleted, added rows are created. Enforces:
- team-lead role
- CONFIRMED status
- defaultAttendeeCap
- team-membership of every supplied userId
- cutoff = LIVE_FINAL.windowOpenAt − attendeeEditCutoffHours (default 48)
Audit-logged as FINALIST_EDIT_ATTENDEES with the diff payload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin can un-confirm a CONFIRMED finalist (e.g. to allow a category
quota decrease). Sets status to SUPERSEDED, cascades to drop the active
mentor assignment (if any) with droppedBy='finalist_unconfirmed' and
the reason embedded. Mentor receives a MENTEE_DROPPED notification.
Already-completed assignments are preserved untouched.
- New components/admin/grand-finale/finalist-slots-card: per-category
quota editor with confirmed/pending counts, dirty-tracking, save button.
Renders an empty editor for both Startup and Business Concept categories
even when no quota exists yet.
- New components/admin/grand-finale/waitlist-card: per-category ranked
waitlist display with status badges + manual-promote AlertDialog
(audit-logged via FINALIST_MANUAL_PROMOTE).
- Round detail page: embeds both cards conditionally when
roundType === 'LIVE_FINAL'.
- New finalist router queries: listQuotas, listCategoryCounts (groupBy
on category+status), listWaitlist (rank-ordered with project relation).
Smoke-tested: setting Startup quota to 3 persists to DB; UI renders
quota editor and waitlist card cleanly with empty state.
- expirePendingPastDeadline service: scans PENDING confirmations past
deadline, marks each EXPIRED + audit-logs, then promotes the next
waitlist entry per affected category (using each program's grand-final
round configJson for windowHours).
- /api/cron/finalist-confirmations: hourly cron entrypoint (CRON_SECRET
header gate), wraps the service.
- finalist.addToWaitlist: insert at a specific rank, shifting later
entries down (transactional).
- finalist.reorderWaitlist: rewrite a category's rank order in one go,
using a temp-rank trick to avoid unique-constraint conflicts mid-update.
- finalist.manualPromote: out-of-rank-order admin promote with audit log
(FINALIST_MANUAL_PROMOTE) + fresh confirmation email.
2 new tests. Suite at 14/14 for finalist-confirmation.
- finalist.getByToken: public lookup of a confirmation by signed token,
with all the data the public page needs (project, team members, current
state). Throws on expired/tampered tokens.
- finalist.confirm: validates team membership of every selected user,
checks against program.defaultAttendeeCap, atomically writes
status=CONFIRMED + AttendingMember rows in a transaction.
- finalist.decline: captures optional reason, then promotes the next
WAITING waitlist entry in the same category (no-op if waitlist empty).
Resolves the new windowHours from the LIVE_FINAL round configJson.
- promoteNextWaitlistEntry service: encapsulates the cascade (mark
PROMOTED, create fresh PENDING confirmation, send email).
- New service module createPendingConfirmation: writes a PENDING
FinalistConfirmation row with a signed token whose exp matches the
computed deadline.
- selectFinalists admin mutation: reads windowHours from the round's
configJson.confirmationWindowHours (default 24), validates category
match + quota, then creates one confirmation per selected project
and sends a notification email to the team lead. Email failures are
logged but never roll back the row creation.
- New email helpers: getFinalistConfirmationTemplate +
sendFinalistConfirmationEmail.