feat(final-docs): notify mentor when a finalist uploads a Grand Final document

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-09 16:07:28 +02:00
parent 146691be00
commit f61dcfa89a
2 changed files with 47 additions and 1 deletions

View File

@@ -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
}),

View File

@@ -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()
})
})