import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, createTestCompetition, createTestRound, cleanupTestData, uid, } from '../helpers' import { finalistRouter } from '../../src/server/routers/finalist' beforeAll(() => { process.env.NEXTAUTH_SECRET = 'test-secret-for-finalist-tokens' process.env.NEXTAUTH_URL = 'http://localhost:3001' }) describe('finalist.selectFinalists', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId } } }, }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } }) await prisma.finalistSlotQuota.deleteMany({ where: { programId } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) it('creates PENDING confirmations with unique tokens for each selected project', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `select-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) const p1 = await createTestProject(program.id, { title: 'P1', competitionCategory: 'STARTUP' }) const p2 = await createTestProject(program.id, { title: 'P2', competitionCategory: 'STARTUP' }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const result = await caller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [p1.id, p2.id], roundId: round.id, }) expect(result.created).toBe(2) const confirmations = await prisma.finalistConfirmation.findMany({ where: { project: { programId: program.id } }, }) expect(confirmations).toHaveLength(2) expect(new Set(confirmations.map((c) => c.token)).size).toBe(2) for (const c of confirmations) { expect(c.status).toBe('PENDING') expect(c.deadline.getTime()).toBeGreaterThan(Date.now() + 23 * 3_600_000) expect(c.deadline.getTime()).toBeLessThan(Date.now() + 25 * 3_600_000) } }) it('uses round configJson.confirmationWindowHours when configured', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `select-window-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 48 }, }) const p1 = await createTestProject(program.id, { title: 'P1', competitionCategory: 'STARTUP' }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [p1.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findFirstOrThrow({ where: { projectId: p1.id }, }) expect(confirmation.deadline.getTime()).toBeGreaterThan(Date.now() + 47 * 3_600_000) expect(confirmation.deadline.getTime()).toBeLessThan(Date.now() + 49 * 3_600_000) }) it('defaults to 24h when confirmationWindowHours is not in configJson', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `select-default-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: {}, }) const p1 = await createTestProject(program.id, { title: 'P1', competitionCategory: 'STARTUP' }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [p1.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findFirstOrThrow({ where: { projectId: p1.id }, }) expect(confirmation.deadline.getTime()).toBeGreaterThan(Date.now() + 23 * 3_600_000) expect(confirmation.deadline.getTime()).toBeLessThan(Date.now() + 25 * 3_600_000) }) it('rejects selecting more projects than the category quota', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `select-quota-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) await prisma.finalistSlotQuota.create({ data: { programId: program.id, category: 'STARTUP', quota: 1 }, }) const p1 = await createTestProject(program.id, { title: 'P1', competitionCategory: 'STARTUP' }) const p2 = await createTestProject(program.id, { title: 'P2', competitionCategory: 'STARTUP' }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await expect( caller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [p1.id, p2.id], roundId: round.id, }), ).rejects.toThrow(/exceeds quota/i) }) it('rejects projects whose category does not match', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const program = await createTestProgram({ name: `select-cat-${uid()}` }) programIds.push(program.id) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) const p1 = await createTestProject(program.id, { title: 'P1', competitionCategory: 'BUSINESS_CONCEPT', }) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await expect( caller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [p1.id], roundId: round.id, }), ).rejects.toThrow(/category mismatch/i) }) }) describe('finalist.confirm and decline (public)', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId } } }, }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } }) await prisma.waitlistEntry.deleteMany({ where: { programId } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) async function setupPendingConfirmation(programName: string) { const program = await createTestProgram({ name: programName }) programIds.push(program.id) const lead = await prisma.user.create({ data: { id: uid('user'), email: `lead_${uid()}@test.local`, name: 'Team Lead', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(lead.id) const teammate = await prisma.user.create({ data: { id: uid('user'), email: `mate_${uid()}@test.local`, name: 'Teammate', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(teammate.id) const project = await createTestProject(program.id, { title: 'Confirmable Project', competitionCategory: 'STARTUP', }) await prisma.teamMember.createMany({ data: [ { projectId: project.id, userId: lead.id, role: 'LEAD' }, { projectId: project.id, userId: teammate.id, role: 'MEMBER' }, ], }) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) return { program, lead, teammate, project, round } } it('confirm with valid token + valid attendees succeeds', async () => { const { program, lead, teammate, project } = await setupPendingConfirmation( `confirm-ok-${uid()}`, ) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await publicCaller.confirm({ token: confirmation.token, attendingUserIds: [lead.id, teammate.id], visaFlags: { [teammate.id]: true }, }) const updated = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { id: confirmation.id }, include: { attendingMembers: true }, }) expect(updated.status).toBe('CONFIRMED') expect(updated.attendingMembers).toHaveLength(2) const visaForTeammate = updated.attendingMembers.find((a) => a.userId === teammate.id) expect(visaForTeammate?.needsVisa).toBe(true) const visaForLead = updated.attendingMembers.find((a) => a.userId === lead.id) expect(visaForLead?.needsVisa).toBe(false) }) it('confirm rejects userIds not in the project team', async () => { const { program, project } = await setupPendingConfirmation(`confirm-bad-${uid()}`) const outsider = await prisma.user.create({ data: { id: uid('user'), email: `outsider_${uid()}@test.local`, name: 'Outsider', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(outsider.id) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await expect( publicCaller.confirm({ token: confirmation.token, attendingUserIds: [outsider.id], visaFlags: {}, }), ).rejects.toThrow(/not a team member/i) }) it('confirm rejects when attendee count > program.defaultAttendeeCap', async () => { const { program, lead, teammate, project } = await setupPendingConfirmation( `confirm-cap-${uid()}`, ) await prisma.program.update({ where: { id: program.id }, data: { defaultAttendeeCap: 1 } }) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await expect( publicCaller.confirm({ token: confirmation.token, attendingUserIds: [lead.id, teammate.id], visaFlags: {}, }), ).rejects.toThrow(/attendee cap/i) }) it('decline marks the confirmation DECLINED with optional reason', async () => { const { program, project } = await setupPendingConfirmation(`decline-${uid()}`) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await publicCaller.decline({ token: confirmation.token, reason: 'team disbanded' }) const updated = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { id: confirmation.id }, }) expect(updated.status).toBe('DECLINED') expect(updated.declineReason).toBe('team disbanded') expect(updated.declinedAt).not.toBeNull() }) it('decline triggers next waitlist entry promotion', async () => { const { program, project } = await setupPendingConfirmation(`decline-cascade-${uid()}`) // Create a waitlist entry for a different project in the same category const backupProject = await createTestProject(program.id, { title: 'Backup', competitionCategory: 'STARTUP', }) const backupLead = await prisma.user.create({ data: { id: uid('user'), email: `backup_${uid()}@test.local`, name: 'Backup Lead', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(backupLead.id) await prisma.teamMember.create({ data: { projectId: backupProject.id, userId: backupLead.id, role: 'LEAD' }, }) const waitlistEntry = await prisma.waitlistEntry.create({ data: { programId: program.id, projectId: backupProject.id, category: 'STARTUP', rank: 1, status: 'WAITING', }, }) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const original = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await publicCaller.decline({ token: original.token }) // Backup project should now have a PENDING confirmation const promoted = await prisma.finalistConfirmation.findUnique({ where: { projectId: backupProject.id }, }) expect(promoted).not.toBeNull() expect(promoted?.status).toBe('PENDING') expect(promoted?.promotedFromWaitlistEntryId).toBe(waitlistEntry.id) const updatedEntry = await prisma.waitlistEntry.findUniqueOrThrow({ where: { id: waitlistEntry.id }, }) expect(updatedEntry.status).toBe('PROMOTED') }) it('decline succeeds even when waitlist is empty', async () => { const { program, project } = await setupPendingConfirmation(`decline-empty-${uid()}`) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) const round = await prisma.round.findFirstOrThrow({ where: { competition: { programId: program.id } }, }) await adminCaller.selectFinalists({ programId: program.id, category: 'STARTUP', projectIds: [project.id], roundId: round.id, }) const confirmation = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { projectId: project.id }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await expect(publicCaller.decline({ token: confirmation.token })).resolves.toEqual({ ok: true, }) }) it('expirePendingPastDeadline marks expired confirmations and promotes next waitlist entry', async () => { const { expirePendingPastDeadline } = await import( '../../src/server/services/finalist-confirmation' ) const { program, project } = await setupPendingConfirmation(`expire-${uid()}`) // Create the original PENDING confirmation with a past deadline const originalId = `cmfc_exp_${uid()}` const expiredExp = Math.floor(Date.now() / 1000) - 60 const { signFinalistToken } = await import('../../src/lib/finalist-token') const originalToken = signFinalistToken({ confirmationId: originalId, exp: expiredExp }) await prisma.finalistConfirmation.create({ data: { id: originalId, projectId: project.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() - 60_000), token: originalToken, }, }) // And a waitlist entry to promote const backupProject = await createTestProject(program.id, { title: 'Cron Backup', competitionCategory: 'STARTUP', }) const backupLead = await prisma.user.create({ data: { id: uid('user'), email: `cronlead_${uid()}@test.local`, name: 'Cron Lead', role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(backupLead.id) await prisma.teamMember.create({ data: { projectId: backupProject.id, userId: backupLead.id, role: 'LEAD' }, }) const waitlistEntry = await prisma.waitlistEntry.create({ data: { programId: program.id, projectId: backupProject.id, category: 'STARTUP', rank: 1, status: 'WAITING', }, }) const result = await expirePendingPastDeadline(prisma) expect(result.expired).toBeGreaterThanOrEqual(1) expect(result.promoted).toBeGreaterThanOrEqual(1) const updated = await prisma.finalistConfirmation.findUniqueOrThrow({ where: { id: originalId }, }) expect(updated.status).toBe('EXPIRED') expect(updated.expiredAt).not.toBeNull() const promoted = await prisma.finalistConfirmation.findUnique({ where: { projectId: backupProject.id }, }) expect(promoted).not.toBeNull() expect(promoted?.status).toBe('PENDING') const updatedEntry = await prisma.waitlistEntry.findUniqueOrThrow({ where: { id: waitlistEntry.id }, }) expect(updatedEntry.status).toBe('PROMOTED') }) it('manualPromote bypasses rank order and audit-logs the override', async () => { const { program } = await setupPendingConfirmation(`manual-${uid()}`) const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) // Three ranked waitlist entries const entries = [] for (let i = 0; i < 3; i++) { const p = await createTestProject(program.id, { title: `Rank ${i + 1}`, competitionCategory: 'STARTUP', }) const lead = await prisma.user.create({ data: { id: uid('user'), email: `rank${i}_${uid()}@test.local`, name: `Rank ${i + 1} Lead`, role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) userIds.push(lead.id) await prisma.teamMember.create({ data: { projectId: p.id, userId: lead.id, role: 'LEAD' }, }) const entry = await prisma.waitlistEntry.create({ data: { programId: program.id, projectId: p.id, category: 'STARTUP', rank: i + 1, status: 'WAITING', }, }) entries.push(entry) } // Manually promote rank #3 (out of order) const adminCaller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await adminCaller.manualPromote({ waitlistEntryId: entries[2].id, windowHours: 24 }) const promoted = await prisma.waitlistEntry.findUniqueOrThrow({ where: { id: entries[2].id } }) expect(promoted.status).toBe('PROMOTED') const stillWaiting = await prisma.waitlistEntry.findUniqueOrThrow({ where: { id: entries[0].id }, }) expect(stillWaiting.status).toBe('WAITING') // Confirmation row exists const confirmation = await prisma.finalistConfirmation.findUnique({ where: { projectId: entries[2].projectId }, }) expect(confirmation).not.toBeNull() expect(confirmation?.promotedFromWaitlistEntryId).toBe(entries[2].id) }) it('getByToken rejects expired tokens', async () => { const { program, project } = await setupPendingConfirmation(`confirm-expired-${uid()}`) // Manually create a confirmation with a past deadline + signed-expired token const { signFinalistToken } = await import('../../src/lib/finalist-token') const id = `cmfc_expired_${uid()}` const expiredExp = Math.floor(Date.now() / 1000) - 60 const token = signFinalistToken({ confirmationId: id, exp: expiredExp }) await prisma.finalistConfirmation.create({ data: { id, projectId: project.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() - 60_000), token, }, }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await expect(publicCaller.getByToken({ token })).rejects.toThrow(/expired/i) // Cleanup await prisma.finalistConfirmation.delete({ where: { id } }) void program }) })