diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index c4e6d0f..6effb46 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -97,6 +97,7 @@ import { FinalistSlotsCard } from '@/components/admin/grand-finale/finalist-slot import { WaitlistCard } from '@/components/admin/grand-finale/waitlist-card' import { FinalistEnrollmentCard } from '@/components/admin/grand-finale/finalist-enrollment-card' import { FinalDocsReminderButton } from '@/components/admin/grand-finale/final-docs-reminder-button' +import { FinalDocsUploadsToggle } from '@/components/admin/grand-finale/final-docs-uploads-toggle' import { RankingDashboard } from '@/components/admin/round/ranking-dashboard' import { CoverageReport } from '@/components/admin/assignment/coverage-report' import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet' @@ -1530,14 +1531,17 @@ export default function RoundDetailPage() { {isGrandFinale && programId && ( <> -
- - +
+ +
+ + +
diff --git a/src/components/admin/grand-finale/final-docs-uploads-toggle.tsx b/src/components/admin/grand-finale/final-docs-uploads-toggle.tsx new file mode 100644 index 0000000..f2119d2 --- /dev/null +++ b/src/components/admin/grand-finale/final-docs-uploads-toggle.tsx @@ -0,0 +1,37 @@ +'use client' + +import { trpc } from '@/lib/trpc/client' +import { Switch } from '@/components/ui/switch' +import { Label } from '@/components/ui/label' +import { toast } from 'sonner' + +/** + * Admin toggle: whether finalist teams may upload *revised* grand-final documents. + * Off by default — judges always see the teams' existing prior-round submissions + * regardless; this only controls whether teams are prompted/allowed to upload new + * revised versions (and whether the upload reminder cron runs). + */ +export function FinalDocsUploadsToggle({ roundId }: { roundId: string }) { + const utils = trpc.useUtils() + const { data } = trpc.finalist.getRevisedUploadSetting.useQuery({ roundId }) + const set = trpc.finalist.setRevisedUploadSetting.useMutation({ + onSuccess: (r) => { + toast.success(r.enabled ? 'Finalist revised uploads enabled' : 'Finalist revised uploads disabled') + utils.finalist.getRevisedUploadSetting.invalidate({ roundId }) + }, + onError: (e) => toast.error(e.message), + }) + return ( +
+ set.mutate({ roundId, enabled: v })} + /> + +
+ ) +} diff --git a/src/components/finals/finals-documents-review.tsx b/src/components/finals/finals-documents-review.tsx index 0f59293..cb6c913 100644 --- a/src/components/finals/finals-documents-review.tsx +++ b/src/components/finals/finals-documents-review.tsx @@ -60,10 +60,6 @@ export function FinalsDocumentsReview() { ) } - const fmt = new Intl.DateTimeFormat(undefined, { - dateStyle: 'long', - timeStyle: 'short', - }) return (
@@ -71,10 +67,8 @@ export function FinalsDocumentsReview() { Finalist Documents

- {data.submittedCount} of {data.totalCount} teams complete - {data.round.deadline - ? ` · due ${fmt.format(new Date(data.round.deadline))}` - : ''} + {data.totalCount} finalist team{data.totalCount === 1 ? '' : 's'} · every file each team + has submitted across all rounds

{data.teams.map((team) => ( @@ -82,61 +76,45 @@ export function FinalsDocumentsReview() { {team.teamName}
- {team.category && ( - {team.category} - )} - - {team.submitted ? 'Complete' : 'Incomplete'} + {team.category && {team.category}} + + {team.files.length} file{team.files.length === 1 ? '' : 's'}
- {team.documents.map((doc) => ( -
-
- - {doc.requirementName} + {team.files.length === 0 && ( +

+ No files submitted. +

+ )} + {team.files.map((f) => ( +
+
+ + + {f.docLabel} - {doc.file && ( - + +
+
+ + {f.roundLabel} + + {f.isFinaleUpload && ( + + Revised for finals + )}
- {doc.file ? ( - - ) : ( -

- Not yet uploaded -

- )} +
))} diff --git a/src/server/routers/applicant.ts b/src/server/routers/applicant.ts index 1330080..f9b6f3d 100644 --- a/src/server/routers/applicant.ts +++ b/src/server/routers/applicant.ts @@ -9,7 +9,7 @@ import { sendStyledNotificationEmail, sendTeamMemberInviteEmail } from '@/lib/em import { logAudit } from '@/server/utils/audit' import { createNotification, notifyProjectMentors, NotificationTypes } from '../services/in-app-notification' import { checkRequirementsAndTransition, triggerInProgressOnActivity, transitionProject, isTerminalState } from '../services/round-engine' -import { getFinalDocumentStatusForProject } from '../services/final-documents' +import { getFinalDocumentStatusForProject, finalistUploadsEnabled } from '../services/final-documents' import { EvaluationConfigSchema, MentoringConfigSchema } from '@/types/competition-configs' import type { PrismaClient, Prisma, RoundType } from '@prisma/client' @@ -337,12 +337,15 @@ export const applicantRouter = router({ if (input.roundId) { const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, - select: { name: true, status: true, roundType: true, finalizedAt: true }, + select: { name: true, status: true, roundType: true, finalizedAt: true, configJson: true }, }) if (round) { const uploadable = - round.status === 'ROUND_ACTIVE' || - (round.roundType === 'LIVE_FINAL' && round.status === 'ROUND_DRAFT' && !round.finalizedAt) + round.roundType === 'LIVE_FINAL' + ? !round.finalizedAt && + (round.status === 'ROUND_DRAFT' || round.status === 'ROUND_ACTIVE') && + finalistUploadsEnabled(round.configJson) + : round.status === 'ROUND_ACTIVE' if (!uploadable) { throw new TRPCError({ code: 'BAD_REQUEST', @@ -568,12 +571,15 @@ export const applicantRouter = router({ if (file.roundId) { const round = await ctx.prisma.round.findUnique({ where: { id: file.roundId }, - select: { status: true, roundType: true, finalizedAt: true }, + select: { status: true, roundType: true, finalizedAt: true, configJson: true }, }) if (round) { const modifiable = - round.status === 'ROUND_ACTIVE' || - (round.roundType === 'LIVE_FINAL' && round.status === 'ROUND_DRAFT' && !round.finalizedAt) + round.roundType === 'LIVE_FINAL' + ? !round.finalizedAt && + (round.status === 'ROUND_DRAFT' || round.status === 'ROUND_ACTIVE') && + finalistUploadsEnabled(round.configJson) + : round.status === 'ROUND_ACTIVE' if (!modifiable) { throw new TRPCError({ code: 'BAD_REQUEST', @@ -1472,6 +1478,7 @@ export const applicantRouter = router({ slug: true, roundType: true, windowCloseAt: true, + configJson: true, specialAwardId: true, specialAward: { select: { name: true } }, }, @@ -1490,8 +1497,9 @@ export const applicantRouter = router({ openRounds = allActiveRounds .filter((r) => { - // LIVE_FINAL (grand-final documents) only shows to enrolled finalists. - if (r.roundType === 'LIVE_FINAL' && !projectRoundIds.has(r.id)) return false + // LIVE_FINAL (grand-final documents) only shows to enrolled finalists, + // and only when the admin has enabled revised uploads. + if (r.roundType === 'LIVE_FINAL' && (!projectRoundIds.has(r.id) || !finalistUploadsEnabled(r.configJson))) return false // Award round project isn't in → hide if (r.specialAwardId && !projectRoundIds.has(r.id)) return false // Main round when project is in award track and has no state in this round → hide diff --git a/src/server/routers/finalist.ts b/src/server/routers/finalist.ts index 1c6cec8..73c1ad0 100644 --- a/src/server/routers/finalist.ts +++ b/src/server/routers/finalist.ts @@ -1690,4 +1690,37 @@ export const finalistRouter = router({ if (!allowed) throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have access to the finalist documents review.' }) return listFinalistDocumentsForReview(ctx.prisma, input.programId) }), + + /** Read whether finalists may upload revised grand-final documents (admin toggle). */ + getRevisedUploadSetting: adminProcedure + .input(z.object({ roundId: z.string() })) + .query(async ({ ctx, input }) => { + const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, select: { configJson: true } }) + const cfg = (round?.configJson ?? {}) as { allowFinalistRevisedUploads?: boolean } + return { enabled: !!cfg.allowFinalistRevisedUploads } + }), + + /** Toggle whether finalists may upload revised grand-final documents (admin setting on the LIVE_FINAL round). */ + setRevisedUploadSetting: adminProcedure + .input(z.object({ roundId: z.string(), enabled: z.boolean() })) + .mutation(async ({ ctx, input }) => { + const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, select: { configJson: true, roundType: true } }) + if (!round || round.roundType !== 'LIVE_FINAL') { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Not a grand-final round' }) + } + const cfg = (round.configJson ?? {}) as Record + await ctx.prisma.round.update({ + where: { id: input.roundId }, + data: { configJson: { ...cfg, allowFinalistRevisedUploads: input.enabled } }, + }) + await logAudit({ + prisma: ctx.prisma, + userId: ctx.user.id, + action: 'FINALIST_REVISED_UPLOADS_TOGGLED', + entityType: 'Round', + entityId: input.roundId, + detailsJson: { enabled: input.enabled }, + }) + return { ok: true, enabled: input.enabled } + }), }) diff --git a/src/server/services/final-documents.ts b/src/server/services/final-documents.ts index 797bd29..e0a9078 100644 --- a/src/server/services/final-documents.ts +++ b/src/server/services/final-documents.ts @@ -31,10 +31,20 @@ export async function getOpenFinaleRound(prisma: PrismaClient, programId: string return prisma.round.findFirst({ where: { competition: { programId }, roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null }, orderBy: { sortOrder: 'desc' }, - select: { id: true, name: true, windowCloseAt: true }, + select: { id: true, name: true, windowCloseAt: true, configJson: true }, }) } +/** + * Whether finalist teams are allowed to upload *revised* documents for the + * grand final. This is an admin toggle on the LIVE_FINAL round's configJson. + * When false (default), judges still see the teams' existing prior-round + * submissions, but teams are not prompted/able to upload anything new. + */ +export function finalistUploadsEnabled(configJson: unknown): boolean { + return !!(configJson as { allowFinalistRevisedUploads?: boolean } | null)?.allowFinalistRevisedUploads +} + /** * Per-project grand-final document status. Returns null unless the project is * enrolled (ProjectRoundState) in the program's active LIVE_FINAL round. @@ -50,7 +60,8 @@ export async function getFinalDocumentStatusForProject( if (!project) return null const round = await getOpenFinaleRound(prisma, project.programId) - if (!round) return null + // Banner / upload status only applies when the admin has enabled revised uploads. + if (!round || !finalistUploadsEnabled(round.configJson)) return null const enrolled = await prisma.projectRoundState.findFirst({ where: { projectId, roundId: round.id }, @@ -125,7 +136,7 @@ export async function sendManualFinalDocReminders( opts: { programId: string; projectIds?: string[]; actorId: string }, ): Promise<{ sent: number }> { const round = await getOpenFinaleRound(prisma, opts.programId) - if (!round) return { sent: 0 } + if (!round || !finalistUploadsEnabled(round.configJson)) return { sent: 0 } const states = await prisma.projectRoundState.findMany({ where: { roundId: round.id, ...(opts.projectIds ? { projectId: { in: opts.projectIds } } : {}) }, @@ -170,6 +181,8 @@ export async function sendDueFinalDocReminders(prisma: PrismaClient): Promise<{ let remindersSent = 0 for (const round of rounds) { if (!round.windowCloseAt) continue + // Only chase teams to upload when the admin has enabled revised uploads. + if (!finalistUploadsEnabled(round.configJson)) continue const cfg = (round.configJson ?? {}) as { finalDocsReminderHoursBeforeDeadline?: number } const windowMs = (cfg.finalDocsReminderHoursBeforeDeadline ?? 48) * 3_600_000 const isDue = round.windowCloseAt.getTime() <= now.getTime() + windowMs && round.windowCloseAt.getTime() > now.getTime() @@ -206,62 +219,99 @@ export async function sendDueFinalDocReminders(prisma: PrismaClient): Promise<{ return { remindersSent } } -export type ReviewDocument = { requirementId: string; requirementName: string; file: { id: string; fileName: string; mimeType: string; url: string } | null } -export type ReviewTeam = { projectId: string; teamName: string; category: string | null; documents: ReviewDocument[]; submitted: boolean } -export type ReviewPayload = { round: { id: string; name: string; deadline: Date | null }; totalCount: number; submittedCount: number; teams: ReviewTeam[] } +export type ReviewFile = { + id: string + fileName: string + mimeType: string + url: string + docLabel: string // requirement name if known, else a humanized file type + roundLabel: string // which round this was submitted in + roundSort: number + isFinaleUpload: boolean // uploaded directly to the LIVE_FINAL round (a revised "final") + createdAt: Date +} +export type ReviewTeam = { projectId: string; teamName: string; category: string | null; files: ReviewFile[] } +export type ReviewPayload = { round: { id: string; name: string; deadline: Date | null }; totalCount: number; teams: ReviewTeam[] } + +function humanizeFileType(t: string | null | undefined): string { + switch (t) { + case 'EXEC_SUMMARY': return 'Executive Summary' + case 'BUSINESS_PLAN': return 'Business Plan' + case 'PRESENTATION': return 'Presentation' + case 'VIDEO_PITCH': return 'Video Pitch' + case 'VIDEO': return 'Video' + case 'SUPPORTING_DOC': return 'Supporting Document' + default: return 'Document' + } +} /** - * Read-only review payload of every finalist team enrolled in the program's - * active LIVE_FINAL round, with their uploaded grand-final documents. Each - * present file carries a server-generated GET presigned URL (1h) so finale - * judges — who are not assignment-gated through file.getDownloadUrl — can open - * the documents directly in the browser. + * Read-only review payload for finale judges: every finalist team enrolled in + * the program's LIVE_FINAL round, with ALL of their submitted files across every + * round (pitch deck, executive summary, business plan, videos, plus any revised + * finals uploads). Each file carries a server-generated GET presigned URL (1h) + * so finale judges — who are not assignment-gated through file.getDownloadUrl — + * can open the documents directly. This is NOT gated on the upload toggle: + * judges can always review the teams' existing submissions. */ export async function listFinalistDocumentsForReview(prisma: PrismaClient, programId: string): Promise { const round = await getOpenFinaleRound(prisma, programId) - if (!round) return { round: { id: '', name: '', deadline: null }, totalCount: 0, submittedCount: 0, teams: [] } + if (!round) return { round: { id: '', name: '', deadline: null }, totalCount: 0, teams: [] } - const requirements = await prisma.fileRequirement.findMany({ where: { roundId: round.id }, orderBy: { sortOrder: 'asc' }, select: { id: true, name: true } }) const states = await prisma.projectRoundState.findMany({ where: { roundId: round.id }, select: { project: { select: { id: true, title: true, teamName: true, competitionCategory: true } } }, }) + const projectIds = states.map((s) => s.project.id) - const teams: ReviewTeam[] = [] - for (const { project } of states) { - const files = await prisma.projectFile.findMany({ - where: { projectId: project.id, roundId: round.id, requirementId: { in: requirements.map((r) => r.id) } }, - orderBy: { createdAt: 'desc' }, - select: { id: true, requirementId: true, fileName: true, mimeType: true, bucket: true, objectKey: true }, - }) - const byReq = new Map() - for (const f of files) if (f.requirementId && !byReq.has(f.requirementId)) byReq.set(f.requirementId, f) + // Every file these teams have submitted, in any round. + const allFiles = await prisma.projectFile.findMany({ + where: { projectId: { in: projectIds } }, + orderBy: { createdAt: 'desc' }, + select: { + id: true, projectId: true, fileName: true, mimeType: true, fileType: true, + bucket: true, objectKey: true, createdAt: true, roundId: true, + requirement: { select: { name: true, round: { select: { name: true, sortOrder: true } } } }, + }, + }) - const documents: ReviewDocument[] = [] - for (const r of requirements) { - const f = byReq.get(r.id) - documents.push({ - requirementId: r.id, - requirementName: r.name, - file: f ? { id: f.id, fileName: f.fileName, mimeType: f.mimeType, url: await getPresignedUrl(f.bucket, f.objectKey, 'GET', 3600) } : null, - }) + // Resolve round names for files attached directly to a round (no requirement). + const directRoundIds = [...new Set(allFiles.filter((f) => f.roundId && !f.requirement).map((f) => f.roundId!))] + const directRounds = directRoundIds.length + ? await prisma.round.findMany({ where: { id: { in: directRoundIds } }, select: { id: true, name: true, sortOrder: true } }) + : [] + const roundById = new Map(directRounds.map((r) => [r.id, r])) + + const filesByProject = new Map() + for (const f of allFiles) { + const r = f.requirement?.round ?? (f.roundId ? roundById.get(f.roundId) : null) + const rf: ReviewFile = { + id: f.id, + fileName: f.fileName, + mimeType: f.mimeType, + url: await getPresignedUrl(f.bucket, f.objectKey, 'GET', 3600), + docLabel: f.requirement?.name?.trim() || humanizeFileType(f.fileType), + roundLabel: r?.name ?? '—', + roundSort: r?.sortOrder ?? -1, + isFinaleUpload: f.roundId === round.id, + createdAt: f.createdAt, } - teams.push({ - projectId: project.id, - teamName: project.teamName || project.title, - category: project.competitionCategory, - documents, - submitted: documents.every((d) => d.file !== null), - }) + const list = filesByProject.get(f.projectId) + if (list) list.push(rf) + else filesByProject.set(f.projectId, [rf]) } + const teams: ReviewTeam[] = states.map(({ project }) => ({ + projectId: project.id, + teamName: project.teamName || project.title, + category: project.competitionCategory, + files: (filesByProject.get(project.id) ?? []).sort( + (a, b) => b.roundSort - a.roundSort || a.docLabel.localeCompare(b.docLabel), + ), + })) + teams.sort((a, b) => (a.category || '').localeCompare(b.category || '') || a.teamName.localeCompare(b.teamName)) - return { - round: { id: round.id, name: round.name, deadline: round.windowCloseAt ?? null }, - totalCount: teams.length, - submittedCount: teams.filter((t) => t.submitted).length, - teams, - } + return { round: { id: round.id, name: round.name, deadline: round.windowCloseAt ?? null }, totalCount: teams.length, teams } } /** True if user is admin or a member of the program's open LIVE_FINAL jury group (DRAFT or ACTIVE). */ diff --git a/tests/unit/final-documents.test.ts b/tests/unit/final-documents.test.ts index 2921437..5829448 100644 --- a/tests/unit/final-documents.test.ts +++ b/tests/unit/final-documents.test.ts @@ -25,7 +25,7 @@ import { BUCKET_NAME, generateObjectKey } from '@/lib/minio' const programIds: string[] = [] async function makeFinaleProgram( - opts: { roundStatus?: 'ROUND_ACTIVE' | 'ROUND_DRAFT' | 'ROUND_CLOSED'; closeAt?: Date; skipRequirements?: boolean } = {}, + opts: { roundStatus?: 'ROUND_ACTIVE' | 'ROUND_DRAFT' | 'ROUND_CLOSED'; closeAt?: Date; skipRequirements?: boolean; uploadsEnabled?: boolean } = {}, ) { const program = await createTestProgram() programIds.push(program.id) @@ -35,6 +35,7 @@ async function makeFinaleProgram( status: opts.roundStatus ?? 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: opts.closeAt ?? new Date(Date.now() + 86_400_000), + configJson: { allowFinalistRevisedUploads: opts.uploadsEnabled ?? true }, }) if (opts.skipRequirements) { return { program, comp, round, reqPlan: undefined, reqVideo: undefined } @@ -110,6 +111,14 @@ describe('getFinalDocumentStatusForProject', () => { expect(status).toBeNull() }) + it('returns null when the admin has NOT enabled revised uploads (toggle off)', async () => { + const { program, round } = await makeFinaleProgram({ uploadsEnabled: false }) + 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) @@ -132,7 +141,7 @@ describe('applicant.getFinalDocumentStatus', () => { 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) }) + const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } }) 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) @@ -163,7 +172,7 @@ describe('sendManualFinalDocReminders', () => { 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) }) + const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } }) 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) @@ -189,7 +198,7 @@ describe('sendDueFinalDocReminders', () => { 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 }, + configJson: { finalDocsReminderHoursBeforeDeadline: 48, allowFinalistRevisedUploads: true }, }) 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) @@ -215,7 +224,7 @@ describe('finalist.listReviewDocuments', () => { 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) }) + const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } }) 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' }) @@ -257,7 +266,7 @@ describe('mentor.getProjectFinalDocuments', () => { 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) }) + const round = await createTestRound(comp.id, { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE', sortOrder: 6, windowCloseAt: new Date(Date.now() + 86_400_000), configJson: { allowFinalistRevisedUploads: true } }) 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)