/** * Task 6: Travel + visa attendee emails * Tests that setFlightStatus(CONFIRMED) and updateVisaApplication(status change) * create the right InAppNotification rows for the attending member. */ import { afterAll, describe, expect, it } from 'vitest' import { prisma, createCaller } from '../setup' import { createTestUser, createTestProgram, createTestProject, cleanupTestData, uid, } from '../helpers' import { logisticsRouter } from '../../src/server/routers/logistics' // --------------------------------------------------------------------------- // Shared helpers // --------------------------------------------------------------------------- async function createApplicant(tag: string) { const id = uid('user') return prisma.user.create({ data: { id, email: `${tag}_${id}@test.local`, name: `Test User ${tag}`, role: 'APPLICANT', roles: ['APPLICANT'], status: 'ACTIVE', }, }) } /** * Creates a confirmed attendee with a flight detail row in PENDING status. * Returns all entities needed by the tests. */ async function setupFlightScenario(label: string) { const program = await createTestProgram({ name: `comms-flight-${label}-${uid()}` }) const user = await createApplicant(`flight_${label}`) const project = await createTestProject(program.id, { title: `Project ${label}`, competitionCategory: 'STARTUP', }) await prisma.teamMember.create({ data: { projectId: project.id, userId: user.id, role: 'LEAD' }, }) const confirmation = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, }) const attendingMember = await prisma.attendingMember.create({ data: { confirmationId: confirmation.id, userId: user.id, needsVisa: false }, }) const flightDetail = await prisma.flightDetail.create({ data: { attendingMemberId: attendingMember.id, arrivalAt: new Date('2026-06-28T12:00:00Z'), arrivalFlightNumber: 'AF7400', arrivalAirport: 'NCE', departureAt: new Date('2026-07-01T16:00:00Z'), departureFlightNumber: 'AF7401', departureAirport: 'NCE', status: 'PENDING', }, }) return { program, user, project, confirmation, attendingMember, flightDetail } } /** * Creates a confirmed attendee with a visa application in REQUESTED status. */ async function setupVisaScenario(label: string) { const program = await createTestProgram({ name: `comms-visa-${label}-${uid()}` }) const user = await createApplicant(`visa_${label}`) const project = await createTestProject(program.id, { title: `Visa Project ${label}`, competitionCategory: 'STARTUP', }) await prisma.teamMember.create({ data: { projectId: project.id, userId: user.id, role: 'LEAD' }, }) const confirmation = await prisma.finalistConfirmation.create({ data: { projectId: project.id, category: 'STARTUP', status: 'CONFIRMED', deadline: new Date(Date.now() + 86_400_000), token: `tok_${uid()}`, confirmedAt: new Date(), }, }) const attendingMember = await prisma.attendingMember.create({ data: { confirmationId: confirmation.id, userId: user.id, needsVisa: true }, }) const visaApplication = await prisma.visaApplication.create({ data: { attendingMemberId: attendingMember.id, status: 'REQUESTED' }, }) return { program, user, project, confirmation, attendingMember, visaApplication } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('logistics comms: flight confirmed notification', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { // Clean up InAppNotification rows first for (const userId of userIds) { await prisma.inAppNotification.deleteMany({ where: { userId } }) } for (const programId of programIds) { await prisma.flightDetail.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('creates TRAVEL_CONFIRMED notification when flight status set to CONFIRMED', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, user, flightDetail } = await setupFlightScenario('confirm') programIds.push(program.id) userIds.push(user.id) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.setFlightStatus({ flightDetailId: flightDetail.id, status: 'CONFIRMED' }) const notification = await prisma.inAppNotification.findFirst({ where: { userId: user.id, type: 'TRAVEL_CONFIRMED' }, }) expect(notification).not.toBeNull() expect(notification!.type).toBe('TRAVEL_CONFIRMED') }) it('does NOT create TRAVEL_CONFIRMED notification when status is set to PENDING', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, user, flightDetail } = await setupFlightScenario('pending') programIds.push(program.id) userIds.push(user.id) // First set to CONFIRMED to have a baseline, then revert to PENDING const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) // Start with CONFIRMED so we know a notification would be created await caller.setFlightStatus({ flightDetailId: flightDetail.id, status: 'CONFIRMED' }) // Clear notifications for this user so we can test the PENDING case cleanly await prisma.inAppNotification.deleteMany({ where: { userId: user.id } }) // Now set back to PENDING — must NOT create a new TRAVEL_CONFIRMED notification await caller.setFlightStatus({ flightDetailId: flightDetail.id, status: 'PENDING' }) const notification = await prisma.inAppNotification.findFirst({ where: { userId: user.id, type: 'TRAVEL_CONFIRMED' }, }) expect(notification).toBeNull() }) }) describe('logistics comms: visa status update notification', () => { const programIds: string[] = [] const userIds: string[] = [] afterAll(async () => { // Clean up InAppNotification rows first for (const userId of userIds) { await prisma.inAppNotification.deleteMany({ where: { userId } }) } 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('creates VISA_STATUS_UPDATE notification when visa status updated to GRANTED', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, user, visaApplication } = await setupVisaScenario('granted') programIds.push(program.id) userIds.push(user.id) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) await caller.updateVisaApplication({ id: visaApplication.id, status: 'GRANTED' }) const notification = await prisma.inAppNotification.findFirst({ where: { userId: user.id, type: 'VISA_STATUS_UPDATE' }, }) expect(notification).not.toBeNull() expect(notification!.type).toBe('VISA_STATUS_UPDATE') }) it('does NOT create duplicate VISA_STATUS_UPDATE when status is unchanged', async () => { const admin = await createTestUser('SUPER_ADMIN') userIds.push(admin.id) const { program, user, visaApplication } = await setupVisaScenario('no-dup') programIds.push(program.id) userIds.push(user.id) const caller = createCaller(logisticsRouter, { id: admin.id, email: admin.email, role: 'SUPER_ADMIN', }) // First update: REQUESTED → GRANTED (creates a notification) await caller.updateVisaApplication({ id: visaApplication.id, status: 'GRANTED' }) // Second update: GRANTED → GRANTED again (same status, should NOT create another) await caller.updateVisaApplication({ id: visaApplication.id, status: 'GRANTED' }) const notifications = await prisma.inAppNotification.findMany({ where: { userId: user.id, type: 'VISA_STATUS_UPDATE' }, }) expect(notifications).toHaveLength(1) }) })