/** * Task 3: Admin alerts on confirmation lifecycle * * Verifies that in-app notifications are created for SUPER_ADMIN users * on confirm, decline, and expiry events. */ 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' import { expirePendingPastDeadline } from '../../src/server/services/finalist-confirmation' beforeAll(() => { process.env.NEXTAUTH_SECRET = 'test-secret-for-finalist-tokens' process.env.NEXTAUTH_URL = 'http://localhost:3001' }) describe('finalist admin alert notifications (Task 3)', () => { const programIds: string[] = [] const userIds: string[] = [] let adminUserId: string afterAll(async () => { // Clean up notifications for admin if (adminUserId) { await prisma.inAppNotification.deleteMany({ where: { userId: adminUserId } }) } for (const programId of programIds) { await prisma.inAppNotification.deleteMany({ where: { metadata: { path: ['projectId'], string_contains: '' }, }, }) 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 } } }) } }) /** * Set up a PENDING confirmation with a team lead + teammate, and a * SUPER_ADMIN user to verify notifications against. */ async function setupWithAdmin(programName: string) { // Create the admin once (shared) or per test const admin = await prisma.user.create({ data: { id: uid('user'), email: `admin_${uid()}@test.local`, name: 'Super Admin', role: 'SUPER_ADMIN', roles: ['SUPER_ADMIN'], status: 'ACTIVE', }, }) userIds.push(admin.id) adminUserId = admin.id 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: 'Notification Test 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' }) await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) // Use the admin caller to selectFinalists and get a PENDING confirmation 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 }, }) return { admin, program, lead, teammate, project, confirmation, round } } it('Test 1: finalist.confirm fires FINALIST_CONFIRMED notification to admin', async () => { const { admin, lead, teammate, confirmation } = await setupWithAdmin( `comms-confirm-${uid()}`, ) // Clear any existing notifications for this admin before the action await prisma.inAppNotification.deleteMany({ where: { userId: admin.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: {}, }) const notification = await prisma.inAppNotification.findFirst({ where: { userId: admin.id, type: 'FINALIST_CONFIRMED', }, }) expect(notification).not.toBeNull() expect(notification?.type).toBe('FINALIST_CONFIRMED') }) it('Test 2: finalist.decline fires FINALIST_DECLINED notification to admin', async () => { const { admin, confirmation } = await setupWithAdmin(`comms-decline-${uid()}`) // Clear existing notifications await prisma.inAppNotification.deleteMany({ where: { userId: admin.id } }) const publicCaller = finalistRouter.createCaller({ session: null, prisma, ip: '127.0.0.1', userAgent: 'vitest', } as never) await publicCaller.decline({ token: confirmation.token, reason: 'Cannot attend' }) const notification = await prisma.inAppNotification.findFirst({ where: { userId: admin.id, type: 'FINALIST_DECLINED', }, }) expect(notification).not.toBeNull() expect(notification?.type).toBe('FINALIST_DECLINED') }) it('Test 3: expirePendingPastDeadline fires FINALIST_EXPIRED notification to admin', async () => { // Create a fresh admin for this test (isolated) const admin = await prisma.user.create({ data: { id: uid('user'), email: `admin_expire_${uid()}@test.local`, name: 'Super Admin Expire', role: 'SUPER_ADMIN', roles: ['SUPER_ADMIN'], status: 'ACTIVE', }, }) userIds.push(admin.id) const program = await createTestProgram({ name: `comms-expire-${uid()}` }) programIds.push(program.id) const project = await createTestProject(program.id, { title: 'Expire Test Project', competitionCategory: 'STARTUP', }) const competition = await createTestCompetition(program.id, { status: 'ACTIVE' }) await createTestRound(competition.id, { roundType: 'LIVE_FINAL', configJson: { confirmationWindowHours: 24 }, }) // Manually create a PENDING confirmation with a past deadline const { signFinalistToken } = await import('../../src/lib/finalist-token') const confirmationId = `cmfc_expire_${uid()}` const expiredExp = Math.floor(Date.now() / 1000) - 60 const token = signFinalistToken({ confirmationId, exp: expiredExp }) await prisma.finalistConfirmation.create({ data: { id: confirmationId, projectId: project.id, category: 'STARTUP', status: 'PENDING', deadline: new Date(Date.now() - 60_000), token, }, }) // Clear any existing notifications for this admin await prisma.inAppNotification.deleteMany({ where: { userId: admin.id } }) await expirePendingPastDeadline(prisma) const notification = await prisma.inAppNotification.findFirst({ where: { userId: admin.id, type: 'FINALIST_EXPIRED', }, }) expect(notification).not.toBeNull() expect(notification?.type).toBe('FINALIST_EXPIRED') // Cleanup this test's notifications await prisma.inAppNotification.deleteMany({ where: { userId: admin.id } }) }) })