From f61dcfa89a87cd71b7304dd1a5dfcb271af8b7fe Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 9 Jun 2026 16:07:28 +0200 Subject: [PATCH] feat(final-docs): notify mentor when a finalist uploads a Grand Final document Co-Authored-By: Claude Opus 4.8 (1M context) --- src/server/routers/applicant.ts | 20 +++++++++++++++++++- tests/unit/final-documents.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/server/routers/applicant.ts b/src/server/routers/applicant.ts index 845dd5e..9a699a6 100644 --- a/src/server/routers/applicant.ts +++ b/src/server/routers/applicant.ts @@ -7,7 +7,7 @@ import { generateLogoKey, createStorageProvider, type StorageProviderType } from import { getImageUploadUrl, confirmImageUpload, getImageUrl, deleteImage, type ImageUploadConfig } from '@/server/utils/image-upload' import { sendStyledNotificationEmail, sendTeamMemberInviteEmail } from '@/lib/email' import { logAudit } from '@/server/utils/audit' -import { createNotification } from '../services/in-app-notification' +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 { EvaluationConfigSchema, MentoringConfigSchema } from '@/types/competition-configs' @@ -499,6 +499,24 @@ export const applicantRouter = router({ console.warn('[DocAnalyzer] Post-upload analysis failed:', err)) ).catch(() => {}) + // Notify the team's mentor(s) when a Grand-Final document is uploaded. + if (input.roundId) { + try { + const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, select: { roundType: true } }) + if (round?.roundType === 'LIVE_FINAL') { + await notifyProjectMentors(input.projectId, { + type: NotificationTypes.GRAND_FINAL_DOCS_SUBMITTED, + title: 'Final document uploaded', + message: `A team uploaded a Grand Final document: ${input.fileName}`, + linkUrl: `/mentor/workspace/${input.projectId}`, + metadata: { projectId: input.projectId, fileName: input.fileName }, + }) + } + } catch (e) { + console.error('[final-docs] mentor notify failed', e) + } + } + return file }), diff --git a/tests/unit/final-documents.test.ts b/tests/unit/final-documents.test.ts index e471ea5..16372bf 100644 --- a/tests/unit/final-documents.test.ts +++ b/tests/unit/final-documents.test.ts @@ -20,6 +20,7 @@ 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[] = [] @@ -270,3 +271,30 @@ describe('mentor.getProjectFinalDocuments', () => { 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() + }) +})