feat(finalist): unified enrollFinalists (round membership + confirmation + email/admin-confirm)
- Add `finalist.enrollFinalists` adminProcedure: creates ProjectRoundState in LIVE_FINAL round (skipDuplicates) + resets/creates FinalistConfirmation in one step, with EMAIL and ADMIN_CONFIRM attendee modes. - Extract `confirmAttendanceInTx` helper into finalist-enrollment.ts; reuse from both adminConfirm and enrollFinalists (DRY refactor, all tests green). - Add 4 tests covering EMAIL mode, ADMIN_CONFIRM mode, re-enroll safety, and over-cap rejection. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { CompetitionCategory, Prisma, PrismaClient } from '@prisma/client'
|
||||
import { signFinalistToken } from '@/lib/finalist-token'
|
||||
import { ensureLunchPickForAttendingMember } from './lunch-pick-sync'
|
||||
|
||||
type TxClient = PrismaClient | Prisma.TransactionClient
|
||||
|
||||
@@ -64,3 +65,49 @@ export async function resetOrCreatePendingConfirmation(
|
||||
})
|
||||
return { id, token, deadline, alreadyConfirmed: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared confirm transaction: atomically writes CONFIRMED status + attendee
|
||||
* rows + visa applications + lunch picks.
|
||||
* Called from both `adminConfirm` and `enrollFinalists` (ADMIN_CONFIRM mode).
|
||||
*
|
||||
* Must be called inside a Prisma $transaction block — `tx` is the transaction
|
||||
* client, NOT the top-level prisma client.
|
||||
*/
|
||||
export async function confirmAttendanceInTx(
|
||||
tx: Prisma.TransactionClient,
|
||||
args: {
|
||||
confirmationId: string
|
||||
attendingUserIds: string[]
|
||||
visaFlags: Record<string, boolean>
|
||||
},
|
||||
): Promise<void> {
|
||||
await tx.finalistConfirmation.update({
|
||||
where: { id: args.confirmationId },
|
||||
data: { status: 'CONFIRMED', confirmedAt: new Date() },
|
||||
})
|
||||
await tx.attendingMember.createMany({
|
||||
data: args.attendingUserIds.map((userId) => ({
|
||||
confirmationId: args.confirmationId,
|
||||
userId,
|
||||
needsVisa: args.visaFlags[userId] ?? false,
|
||||
})),
|
||||
})
|
||||
const visaUsers = args.attendingUserIds.filter((uid) => args.visaFlags[uid] === true)
|
||||
if (visaUsers.length > 0) {
|
||||
const created = await tx.attendingMember.findMany({
|
||||
where: { confirmationId: args.confirmationId, userId: { in: visaUsers } },
|
||||
select: { id: true },
|
||||
})
|
||||
await tx.visaApplication.createMany({
|
||||
data: created.map((m) => ({ attendingMemberId: m.id, status: 'REQUESTED' })),
|
||||
})
|
||||
}
|
||||
const allMembers = await tx.attendingMember.findMany({
|
||||
where: { confirmationId: args.confirmationId, userId: { in: args.attendingUserIds } },
|
||||
select: { id: true },
|
||||
})
|
||||
for (const m of allMembers) {
|
||||
await ensureLunchPickForAttendingMember(tx, m.id)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user