import { describe, it, expect, afterAll } from 'vitest' import { TRPCError } from '@trpc/server' import { prisma } from '../setup' import { createTestProgram, createTestCompetition, createTestRound, createTestProject, createTestProjectRoundState, createTestUser, cleanupTestData, uid, } from '../helpers' import { getFinalDocumentStatusForProject, sendManualFinalDocReminders, sendDueFinalDocReminders, } from '@/server/services/final-documents' import * as applicantRouter from '@/server/routers/applicant' import * as finalistRouter from '@/server/routers/finalist' import * as mentorRouter from '@/server/routers/mentor' import { createCaller } from '../setup' import { BUCKET_NAME, generateObjectKey } from '@/lib/minio' const programIds: string[] = [] async function makeFinaleProgram( opts: { roundStatus?: 'ROUND_ACTIVE' | 'ROUND_DRAFT'; closeAt?: Date; skipRequirements?: boolean } = {}, ) { const program = await createTestProgram() programIds.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: opts.roundStatus ?? 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: opts.closeAt ?? new Date(Date.now() + 86_400_000), }) if (opts.skipRequirements) { return { program, comp, round, reqPlan: undefined, reqVideo: undefined } } const reqPlan = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Final Business Plan', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 }, }) const reqVideo = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: '1-minute Video', acceptedMimeTypes: ['video/*'], isRequired: true, sortOrder: 2 }, }) return { program, comp, round, reqPlan, reqVideo } } describe('getFinalDocumentStatusForProject', () => { afterAll(async () => { for (const id of programIds) await cleanupTestData(id) }) it('returns null when the project is not enrolled in the active LIVE_FINAL round', async () => { const { program } = await makeFinaleProgram() const orphan = await createTestProject(program.id) const status = await getFinalDocumentStatusForProject(prisma, orphan.id) expect(status).toBeNull() }) it('returns per-requirement status with none uploaded', async () => { const { program, round } = await makeFinaleProgram() const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const status = await getFinalDocumentStatusForProject(prisma, project.id) expect(status).not.toBeNull() expect(status!.requirements).toHaveLength(2) expect(status!.requirements.every((r) => !r.uploaded)).toBe(true) expect(status!.allRequiredUploaded).toBe(false) expect(status!.deadline?.toISOString()).toBe(round.windowCloseAt!.toISOString()) }) it('marks a requirement uploaded and flips allRequiredUploaded when all present', async () => { const { program, round, reqPlan, reqVideo } = await makeFinaleProgram() const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) for (const [req, type, mime] of [ [reqPlan!, 'BUSINESS_PLAN', 'application/pdf'], [reqVideo!, 'VIDEO', 'video/mp4'], ] as const) { await prisma.projectFile.create({ data: { id: uid('file'), projectId: project.id, roundId: round.id, requirementId: req.id, fileType: type as any, fileName: `f-${req.id}`, mimeType: mime, size: 10, bucket: 'b', objectKey: uid('key'), }, }) } const status = await getFinalDocumentStatusForProject(prisma, project.id) expect(status!.requirements.every((r) => r.uploaded)).toBe(true) expect(status!.allRequiredUploaded).toBe(true) }) it('returns null when the LIVE_FINAL round is not active', async () => { const { program, round } = await makeFinaleProgram({ roundStatus: 'ROUND_DRAFT' }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const status = await getFinalDocumentStatusForProject(prisma, project.id) expect(status).toBeNull() }) it('reports allRequiredUploaded false when the round has no required requirements', async () => { const { program, round } = await makeFinaleProgram({ skipRequirements: true }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const status = await getFinalDocumentStatusForProject(prisma, project.id) expect(status).not.toBeNull() expect(status!.requirements).toHaveLength(0) expect(status!.allRequiredUploaded).toBe(false) }) }) describe('applicant.getFinalDocumentStatus', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) it('returns the status for the caller\'s enrolled finalist project', async () => { const program = await createTestProgram() localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) }) await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const user = await createTestUser('APPLICANT') localUsers.push(user.id) await prisma.teamMember.create({ data: { projectId: project.id, userId: user.id, role: 'LEAD' } }) const caller = createCaller(applicantRouter.applicantRouter, user) const status = await caller.getFinalDocumentStatus() expect(status?.roundId).toBe(round.id) expect(status?.requirements).toHaveLength(1) }) it('returns null when the caller has no project', async () => { const user = await createTestUser('APPLICANT') localUsers.push(user.id) const caller = createCaller(applicantRouter.applicantRouter, user) expect(await caller.getFinalDocumentStatus()).toBeNull() }) }) describe('sendManualFinalDocReminders', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) it('sends a reminder only to finalist teams with missing required docs', async () => { const program = await createTestProgram() localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) }) await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const lead = await createTestUser('APPLICANT') localUsers.push(lead.id) await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } }) const result = await sendManualFinalDocReminders(prisma, { programId: program.id, actorId: lead.id }) expect(result.sent).toBe(1) const notif = await prisma.inAppNotification.findFirst({ where: { userId: lead.id, type: 'GRAND_FINAL_DOCS_REMINDER' } }) expect(notif).not.toBeNull() }) }) describe('sendDueFinalDocReminders', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) it('reminds once when within the window and stamps finalDocsReminderSentAt', async () => { const program = await createTestProgram(); localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 3_600_000), // 1h out → within 48h window configJson: { finalDocsReminderHoursBeforeDeadline: 48 }, }) await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const lead = await createTestUser('APPLICANT'); localUsers.push(lead.id) await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } }) await prisma.finalistConfirmation.create({ data: { id: uid('fc'), projectId: project.id, status: 'CONFIRMED', category: 'STARTUP', deadline: new Date(Date.now() + 3_600_000), token: uid('tok') } }) const first = await sendDueFinalDocReminders(prisma) expect(first.remindersSent).toBe(1) const second = await sendDueFinalDocReminders(prisma) expect(second.remindersSent).toBe(0) // idempotent: already stamped }) }) describe('finalist.listReviewDocuments', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) async function setup() { const program = await createTestProgram() localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const jg = await prisma.juryGroup.create({ data: { id: uid('jg'), competitionId: comp.id, name: 'Finals Jury', slug: uid('jg') } }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) }) await prisma.round.update({ where: { id: round.id }, data: { juryGroupId: jg.id } }) await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id, { competitionCategory: 'STARTUP' }) await createTestProjectRoundState(project.id, round.id) return { program, comp, jg, round, project } } it('admin sees all finalist teams', async () => { const { program } = await setup() const admin = await createTestUser('PROGRAM_ADMIN'); localUsers.push(admin.id) const caller = createCaller(finalistRouter.finalistRouter, admin) const result = await caller.listReviewDocuments({ programId: program.id }) expect(result.teams).toHaveLength(1) expect(result.totalCount).toBe(1) }) it('a finals jury-group member is allowed', async () => { const { program, jg } = await setup() const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id) await prisma.juryGroupMember.create({ data: { juryGroupId: jg.id, userId: juror.id, role: 'MEMBER' } }) const caller = createCaller(finalistRouter.finalistRouter, juror) const result = await caller.listReviewDocuments({ programId: program.id }) expect(result.teams).toHaveLength(1) }) it('a non-finals jury member is forbidden', async () => { const { program } = await setup() const juror = await createTestUser('JURY_MEMBER'); localUsers.push(juror.id) const caller = createCaller(finalistRouter.finalistRouter, juror) await expect(caller.listReviewDocuments({ programId: program.id })).rejects.toThrow(TRPCError) }) }) describe('mentor.getProjectFinalDocuments', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) it('returns status for a project the mentor is assigned to', async () => { const program = await createTestProgram(); localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000) }) await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id) await prisma.mentorAssignment.create({ data: { projectId: project.id, mentorId: mentor.id } }) const caller = createCaller(mentorRouter.mentorRouter, mentor) const status = await caller.getProjectFinalDocuments({ projectId: project.id }) expect(status?.roundId).toBe(round.id) }) it('forbids a mentor not assigned to the project', async () => { const program = await createTestProgram(); localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6 }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id) const caller = createCaller(mentorRouter.mentorRouter, mentor) await expect(caller.getProjectFinalDocuments({ projectId: project.id })).rejects.toThrow() }) }) describe('saveFileMetadata → GRAND_FINAL_DOCS_SUBMITTED', () => { const localPrograms: string[] = [] const localUsers: string[] = [] afterAll(async () => { for (const id of localPrograms) await cleanupTestData(id, localUsers) }) it('notifies the mentor when a finalist uploads a LIVE_FINAL document', async () => { const program = await createTestProgram(); localPrograms.push(program.id) const comp = await createTestCompetition(program.id, { status: 'ACTIVE' }) const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6 }) const req = await prisma.fileRequirement.create({ data: { id: uid('req'), roundId: round.id, name: 'Executive Summary', acceptedMimeTypes: ['application/pdf'], isRequired: true, sortOrder: 1 } }) const project = await createTestProject(program.id) await createTestProjectRoundState(project.id, round.id) const lead = await createTestUser('APPLICANT'); localUsers.push(lead.id) await prisma.teamMember.create({ data: { projectId: project.id, userId: lead.id, role: 'LEAD' } }) const mentor = await createTestUser('MENTOR'); localUsers.push(mentor.id) await prisma.mentorAssignment.create({ data: { projectId: project.id, mentorId: mentor.id } }) const caller = createCaller(applicantRouter.applicantRouter, lead) await caller.saveFileMetadata({ projectId: project.id, fileName: 'exec.pdf', mimeType: 'application/pdf', size: 100, fileType: 'EXEC_SUMMARY', bucket: BUCKET_NAME, objectKey: generateObjectKey(project.title, 'exec.pdf'), roundId: round.id, requirementId: req.id, }) const notif = await prisma.inAppNotification.findFirst({ where: { userId: mentor.id, type: 'GRAND_FINAL_DOCS_SUBMITTED' } }) expect(notif).not.toBeNull() }) })