diff --git a/src/server/routers/mentor.ts b/src/server/routers/mentor.ts index 5ad2f13..500110c 100644 --- a/src/server/routers/mentor.ts +++ b/src/server/routers/mentor.ts @@ -50,6 +50,7 @@ import { signMentorUploadToken, verifyMentorUploadToken, } from '@/lib/mentor-upload-token' +import { getFinalDocumentStatusForProject } from '../services/final-documents' /** * True if the project is enrolled in a MENTORING round that is still @@ -215,6 +216,21 @@ async function assertProjectWorkspaceAccess( } export const mentorRouter = router({ + /** Grand-final document status for a project, for the mentor workspace panel. */ + getProjectFinalDocuments: protectedProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ ctx, input }) => { + const isAdmin = ['SUPER_ADMIN', 'PROGRAM_ADMIN'].includes(ctx.user.role) + if (!isAdmin) { + const [mentorAssignment, teamMembership] = await Promise.all([ + ctx.prisma.mentorAssignment.findFirst({ where: { mentorId: ctx.user.id, projectId: input.projectId }, select: { id: true } }), + ctx.prisma.teamMember.findFirst({ where: { userId: ctx.user.id, projectId: input.projectId }, select: { id: true } }), + ]) + if (!mentorAssignment && !teamMembership) throw new TRPCError({ code: 'FORBIDDEN', message: 'No access to this project' }) + } + return getFinalDocumentStatusForProject(ctx.prisma, input.projectId) + }), + /** * Get AI-suggested mentor matches for a project */ diff --git a/tests/unit/final-documents.test.ts b/tests/unit/final-documents.test.ts index acffe82..bfbe0d0 100644 --- a/tests/unit/final-documents.test.ts +++ b/tests/unit/final-documents.test.ts @@ -17,6 +17,7 @@ import { } 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' const programIds: string[] = [] @@ -209,3 +210,35 @@ describe('finalist.listReviewDocuments', () => { 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() + }) +})