import { afterAll, 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' import { signFinalistToken } from '../../src/lib/finalist-token' async function createApplicant(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', }, }) } async function setup(opts: { programName: string status?: 'PENDING' | 'CONFIRMED' windowOpenAt?: Date attendees?: { lead?: { needsVisa: boolean }; member?: { needsVisa: boolean } } }) { const program = await createTestProgram({ name: opts.programName, defaultAttendeeCap: 3, }) const project = await createTestProject(program.id, { title: 'P', competitionCategory: 'STARTUP', }) const lead = await createApplicant('LEAD') const member = await createApplicant('MEMBER') await prisma.teamMember.createMany({ data: [ { projectId: project.id, userId: lead.id, role: 'LEAD' }, { projectId: project.id, userId: member.id, role: 'MEMBER' }, ], }) const competition = await createTestCompetition(program.id) await createTestRound(competition.id, { roundType: 'LIVE_FINAL', sortOrder: 99, windowOpenAt: opts.windowOpenAt ?? new Date(Date.now() + 30 * 86_400_000), configJson: { confirmationWindowHours: 24 }, }) const confirmation = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: opts.status ?? 'PENDING', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: opts.status === 'CONFIRMED' ? new Date() : null, }, }) // Seed AttendingMember rows when CONFIRMED if (opts.status === 'CONFIRMED' && opts.attendees) { const rows: { confirmationId: string; userId: string; needsVisa: boolean }[] = [] if (opts.attendees.lead) { rows.push({ confirmationId: confirmation.id, userId: lead.id, needsVisa: opts.attendees.lead.needsVisa, }) } if (opts.attendees.member) { rows.push({ confirmationId: confirmation.id, userId: member.id, needsVisa: opts.attendees.member.needsVisa, }) } if (rows.length > 0) await prisma.attendingMember.createMany({ data: rows }) } return { program, project, lead, member, confirmation } } describe('VisaApplication lifecycle', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { for (const programId of programIds) { await prisma.visaApplication.deleteMany({ where: { attendingMember: { confirmation: { project: { programId } } } }, }) await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId } } }, }) await prisma.finalistConfirmation.deleteMany({ where: { project: { programId } } }) await cleanupTestData(programId, []) } if (userIds.length > 0) { await prisma.user.deleteMany({ where: { id: { in: userIds } } }) } }) it('public confirm creates a VisaApplication for each needsVisa=true attendee', async () => { const { program, lead, member, confirmation } = await setup({ programName: `visa-confirm-${uid()}`, }) programIds.push(program.id) userIds.push(lead.id, member.id) const token = signFinalistToken({ confirmationId: confirmation.id, exp: Math.floor(Date.now() / 1000) + 3600, }) // Update the confirmation to use our signed token await prisma.finalistConfirmation.update({ where: { id: confirmation.id }, data: { token }, }) const caller = createCaller(finalistRouter, { id: '', email: '', role: '' } as never) await caller.confirm({ token, attendingUserIds: [lead.id, member.id], visaFlags: { [lead.id]: false, [member.id]: true }, }) const apps = await prisma.visaApplication.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, include: { attendingMember: { select: { userId: true } } }, }) expect(apps).toHaveLength(1) expect(apps[0].attendingMember.userId).toBe(member.id) expect(apps[0].status).toBe('REQUESTED') }) it('adminConfirm creates a VisaApplication for each needsVisa=true attendee', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, lead, member, confirmation } = await setup({ programName: `visa-admin-confirm-${uid()}`, }) programIds.push(program.id) userIds.push(lead.id, member.id) const caller = createCaller(finalistRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.adminConfirm({ confirmationId: confirmation.id, attendingUserIds: [lead.id, member.id], visaFlags: { [lead.id]: true, [member.id]: true }, }) const apps = await prisma.visaApplication.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, }) expect(apps).toHaveLength(2) }) it('editAttendees creates a VisaApplication when an attendee flips to needsVisa=true', async () => { const { program, lead, member, confirmation } = await setup({ programName: `visa-edit-flip-on-${uid()}`, status: 'CONFIRMED', attendees: { lead: { needsVisa: false } }, }) programIds.push(program.id) userIds.push(lead.id, member.id) const caller = createCaller(finalistRouter, { id: lead.id, email: lead.email, role: 'APPLICANT', }) await caller.editAttendees({ confirmationId: confirmation.id, attendingUserIds: [lead.id], visaFlags: { [lead.id]: true }, }) const apps = await prisma.visaApplication.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, include: { attendingMember: { select: { userId: true } } }, }) expect(apps).toHaveLength(1) expect(apps[0].attendingMember.userId).toBe(lead.id) }) it('editAttendees deletes the VisaApplication when an attendee flips to needsVisa=false', async () => { const { program, lead, member, confirmation } = await setup({ programName: `visa-edit-flip-off-${uid()}`, status: 'CONFIRMED', attendees: { lead: { needsVisa: true } }, }) programIds.push(program.id) userIds.push(lead.id, member.id) const leadAttendee = await prisma.attendingMember.findFirstOrThrow({ where: { confirmationId: confirmation.id, userId: lead.id }, }) await prisma.visaApplication.create({ data: { attendingMemberId: leadAttendee.id, status: 'REQUESTED' }, }) const caller = createCaller(finalistRouter, { id: lead.id, email: lead.email, role: 'APPLICANT', }) await caller.editAttendees({ confirmationId: confirmation.id, attendingUserIds: [lead.id], visaFlags: { [lead.id]: false }, }) const apps = await prisma.visaApplication.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, }) expect(apps).toHaveLength(0) }) it('editAttendees preserves an existing VisaApplication when needsVisa stays true', async () => { const { program, lead, member, confirmation } = await setup({ programName: `visa-edit-preserve-${uid()}`, status: 'CONFIRMED', attendees: { lead: { needsVisa: true } }, }) programIds.push(program.id) userIds.push(lead.id, member.id) const leadAttendee = await prisma.attendingMember.findFirstOrThrow({ where: { confirmationId: confirmation.id, userId: lead.id }, }) const seeded = await prisma.visaApplication.create({ data: { attendingMemberId: leadAttendee.id, status: 'APPOINTMENT_BOOKED', notes: 'preserve me', }, }) const caller = createCaller(finalistRouter, { id: lead.id, email: lead.email, role: 'APPLICANT', }) await caller.editAttendees({ confirmationId: confirmation.id, attendingUserIds: [lead.id], visaFlags: { [lead.id]: true }, }) const app = await prisma.visaApplication.findUniqueOrThrow({ where: { id: seeded.id } }) expect(app.status).toBe('APPOINTMENT_BOOKED') expect(app.notes).toBe('preserve me') }) it('removing an attendee cascades the VisaApplication', async () => { const { program, lead, member, confirmation } = await setup({ programName: `visa-cascade-${uid()}`, status: 'CONFIRMED', attendees: { lead: { needsVisa: true }, member: { needsVisa: true } }, }) programIds.push(program.id) userIds.push(lead.id, member.id) const memberAttendee = await prisma.attendingMember.findFirstOrThrow({ where: { confirmationId: confirmation.id, userId: member.id }, }) const leadAttendee = await prisma.attendingMember.findFirstOrThrow({ where: { confirmationId: confirmation.id, userId: lead.id }, }) await prisma.visaApplication.createMany({ data: [ { attendingMemberId: leadAttendee.id, status: 'REQUESTED' }, { attendingMemberId: memberAttendee.id, status: 'REQUESTED' }, ], }) const caller = createCaller(finalistRouter, { id: lead.id, email: lead.email, role: 'APPLICANT', }) await caller.editAttendees({ confirmationId: confirmation.id, attendingUserIds: [lead.id], visaFlags: { [lead.id]: true }, }) const apps = await prisma.visaApplication.findMany({ where: { attendingMember: { confirmationId: confirmation.id } }, include: { attendingMember: { select: { userId: true } } }, }) expect(apps).toHaveLength(1) expect(apps[0].attendingMember.userId).toBe(lead.id) }) })