import { afterAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, createTestCompetition, createTestRound, cleanupTestData, uid, } from '../helpers' import { resetOrCreatePendingConfirmation } from '../../src/server/services/finalist-enrollment' import { finalistRouter } from '../../src/server/routers/finalist' async function createApplicantUser(role: 'LEAD' | 'MEMBER' = 'MEMBER') { const id = uid('user') return prisma.user.create({ data: { id, email: `${id}@test.local`, name: `Test ${role}`, role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) } describe('resetOrCreatePendingConfirmation', () => { const programIds: string[] = [] afterAll(async () => { for (const id of programIds) { await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId: id } } } }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId: id } } }) await cleanupTestData(id, []) } }) it('creates a fresh PENDING row when none exists', async () => { const program = await createTestProgram({ name: `reinvite-new-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' }) const res = await resetOrCreatePendingConfirmation(prisma, { projectId: project.id, category: 'STARTUP', windowHours: 24, }) const row = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { id: res.id } }) expect(row.status).toBe('PENDING') expect(res.alreadyConfirmed).toBe(false) }) it('resets a DECLINED row to a fresh PENDING (no unique-constraint crash)', async () => { const program = await createTestProgram({ name: `reinvite-declined-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' }) await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'DECLINED', deadline: new Date(Date.now() - 1000), token: `tok_${uid()}`, declinedAt: new Date(), declineReason: 'busy' }, }) const res = await resetOrCreatePendingConfirmation(prisma, { projectId: project.id, category: 'STARTUP', windowHours: 24, }) const row = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { id: res.id } }) expect(row.status).toBe('PENDING') expect(row.declinedAt).toBeNull() expect(row.declineReason).toBeNull() expect(res.alreadyConfirmed).toBe(false) }) it('is a no-op flagged alreadyConfirmed when row is CONFIRMED', async () => { const program = await createTestProgram({ name: `reinvite-confirmed-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' }) await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 1000), token: `tok_${uid()}`, confirmedAt: new Date() }, }) const res = await resetOrCreatePendingConfirmation(prisma, { projectId: project.id, category: 'STARTUP', windowHours: 24, }) expect(res.alreadyConfirmed).toBe(true) }) }) // ─── finalist.enrollFinalists ────────────────────────────────────────────── describe('finalist.enrollFinalists', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const id of programIds) { await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId: id } } } }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId: id } } }) await prisma.projectRoundState.deleteMany({ where: { round: { competition: { programId: id } } } }) await cleanupTestData(id, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) async function setupEnrollFixture(programName: string) { const program = await createTestProgram({ name: programName, defaultAttendeeCap: 3 }) const competition = await createTestCompetition(program.id) const mentoringRound = await createTestRound(competition.id, { roundType: 'MENTORING', sortOrder: 60, }) const liveFinalRound = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', sortOrder: 70, configJson: { confirmationWindowHours: 24 }, }) const project = await createTestProject(program.id, { title: 'Enroll Test Project', competitionCategory: 'STARTUP', }) const lead = await createApplicantUser('LEAD') const member = await createApplicantUser('MEMBER') await prisma.teamMember.createMany({ data: [ { projectId: project.id, userId: lead.id, role: 'LEAD' }, { projectId: project.id, userId: member.id, role: 'MEMBER' }, ], }) // Put the project in the MENTORING round (as candidates) await prisma.projectRoundState.create({ data: { projectId: project.id, roundId: mentoringRound.id }, }) return { program, competition, mentoringRound, liveFinalRound, project, lead, member } } it('EMAIL mode: creates PRS in LIVE_FINAL + PENDING confirmation, no attendees', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, liveFinalRound, project, lead, member } = await setupEnrollFixture( `enroll-email-${uid()}`, ) programIds.push(program.id) userIds.push(lead.id, member.id) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const result = await caller.enrollFinalists({ programId: program.id, roundId: liveFinalRound.id, enrollments: [{ projectId: project.id, mode: 'EMAIL' }], }) expect(result.enrolled).toBe(1) expect(result.skipped).toHaveLength(0) const prs = await prisma.projectRoundState.findFirst({ where: { projectId: project.id, roundId: liveFinalRound.id }, }) expect(prs).not.toBeNull() const conf = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) expect(conf.status).toBe('PENDING') const attendeeCount = await prisma.attendingMember.count({ where: { confirmationId: conf.id }, }) expect(attendeeCount).toBe(0) }) it('ADMIN_CONFIRM mode: CONFIRMED with attendee + visa + lunch rows', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, liveFinalRound, project, lead, member } = await setupEnrollFixture( `enroll-adminconfirm-${uid()}`, ) programIds.push(program.id) userIds.push(lead.id, member.id) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const result = await caller.enrollFinalists({ programId: program.id, roundId: liveFinalRound.id, enrollments: [ { projectId: project.id, mode: 'ADMIN_CONFIRM', attendingUserIds: [lead.id, member.id], visaFlags: { [member.id]: true }, }, ], }) expect(result.enrolled).toBe(1) expect(result.adminConfirmed).toBe(1) expect(result.skipped).toHaveLength(0) const conf = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) expect(conf.status).toBe('CONFIRMED') const attendeeCount = await prisma.attendingMember.count({ where: { confirmationId: conf.id }, }) expect(attendeeCount).toBe(2) const visaCount = await prisma.visaApplication.count({ where: { attendingMember: { confirmationId: conf.id } }, }) expect(visaCount).toBe(1) }) it('re-enrolling a DECLINED project resets it without crashing and keeps one PRS row', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, liveFinalRound, project, lead, member } = await setupEnrollFixture( `enroll-reinvite-${uid()}`, ) programIds.push(program.id) userIds.push(lead.id, member.id) // Pre-create a DECLINED confirmation await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'DECLINED', deadline: new Date(Date.now() - 1000), token: `tok_${uid()}`, declinedAt: new Date(), declineReason: 'schedule conflict', }, }) // Pre-create a PRS row (simulating prior enrollment) await prisma.projectRoundState.create({ data: { projectId: project.id, roundId: liveFinalRound.id }, }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) // Re-enroll in EMAIL mode — should reset DECLINED to PENDING without crashing await caller.enrollFinalists({ programId: program.id, roundId: liveFinalRound.id, enrollments: [{ projectId: project.id, mode: 'EMAIL' }], }) const conf = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) expect(conf.status).toBe('PENDING') expect(conf.declinedAt).toBeNull() // Exactly one PRS row (skipDuplicates kept it idempotent) const prsRows = await prisma.projectRoundState.findMany({ where: { projectId: project.id, roundId: liveFinalRound.id }, }) expect(prsRows).toHaveLength(1) }) it('ADMIN_CONFIRM rejects when attendees exceed cap', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, liveFinalRound, project, lead, member } = await setupEnrollFixture( `enroll-overcap-${uid()}`, ) programIds.push(program.id) userIds.push(lead.id, member.id) // Create 2 extra members so we can pass 4 (cap = 3) const extra1 = await createApplicantUser('MEMBER') const extra2 = await createApplicantUser('MEMBER') userIds.push(extra1.id, extra2.id) await prisma.teamMember.createMany({ data: [ { projectId: project.id, userId: extra1.id, role: 'MEMBER' }, { projectId: project.id, userId: extra2.id, role: 'MEMBER' }, ], }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await expect( caller.enrollFinalists({ programId: program.id, roundId: liveFinalRound.id, enrollments: [ { projectId: project.id, mode: 'ADMIN_CONFIRM', attendingUserIds: [lead.id, member.id, extra1.id, extra2.id], // 4 > cap 3 }, ], }), ).rejects.toThrow(/cap/i) }) })