feat(finalist): listEnrollmentCandidates query for enrollment UI

Returns mentoring-round candidates grouped by category with status,
team members, quota and confirmed/pending counts; inLiveFinal flag and
attendeeCap for the enrollment UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-04 15:29:26 +02:00
parent 375aeb08af
commit e80710487c
2 changed files with 320 additions and 0 deletions

View File

@@ -317,3 +317,176 @@ describe('finalist.enrollFinalists', () => {
).rejects.toThrow(/cap/i)
})
})
// ─── finalist.listEnrollmentCandidates ────────────────────────────────────
describe('finalist.listEnrollmentCandidates', () => {
const programIds: string[] = []
const userIds: string[] = []
afterAll(async () => {
for (const id of programIds) {
await prisma.finalistSlotQuota.deleteMany({ where: { programId: id } })
await prisma.attendingMember.deleteMany({ where: { confirmation: { project: { programId: id } } } })
await prisma.finalistConfirmation.deleteMany({ where: { project: { programId: id } } })
await prisma.projectRoundState.deleteMany({ where: { round: { competition: { programId: id } } } })
await cleanupTestData(id, [])
}
if (userIds.length > 0) {
await prisma.user.deleteMany({ where: { id: { in: userIds } } })
}
})
it('returns the STARTUP project under the STARTUP category with inLiveFinal: false and confirmationStatus: null', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const program = await createTestProgram({
name: `list-candidates-${uid()}`,
defaultAttendeeCap: 3,
})
programIds.push(program.id)
const competition = await createTestCompetition(program.id)
const mentoringRound = await createTestRound(competition.id, {
roundType: 'MENTORING',
sortOrder: 60,
})
const liveFinalRound = await createTestRound(competition.id, {
roundType: 'LIVE_FINAL',
sortOrder: 70,
configJson: { confirmationWindowHours: 24 },
})
const project = await createTestProject(program.id, {
title: 'Candidate Project',
competitionCategory: 'STARTUP',
})
const lead = await createApplicantUser('LEAD')
const member = await createApplicantUser('MEMBER')
userIds.push(lead.id, member.id)
await prisma.teamMember.createMany({
data: [
{ projectId: project.id, userId: lead.id, role: 'LEAD' },
{ projectId: project.id, userId: member.id, role: 'MEMBER' },
],
})
// Put project in MENTORING round (this makes it a candidate)
await prisma.projectRoundState.create({
data: { projectId: project.id, roundId: mentoringRound.id },
})
const caller = createCaller(finalistRouter, {
id: admin.id,
email: admin.email,
role: 'SUPER_ADMIN',
})
const result = await caller.listEnrollmentCandidates({ programId: program.id })
// Top-level shape
expect(result.liveFinalRoundId).toBe(liveFinalRound.id)
expect(result.attendeeCap).toBe(3)
// There should be exactly one category entry for STARTUP
const startupCat = result.categories.find((c: { category: string }) => c.category === 'STARTUP')
expect(startupCat).toBeDefined()
expect(startupCat!.quota).toBeNull() // no quota set
expect(startupCat!.confirmedCount).toBe(0)
expect(startupCat!.pendingCount).toBe(0)
// One candidate
expect(startupCat!.candidates).toHaveLength(1)
const candidate = startupCat!.candidates[0]
expect(candidate.projectId).toBe(project.id)
expect(candidate.title).toBe('Candidate Project')
expect(candidate.inLiveFinal).toBe(false)
expect(candidate.confirmationStatus).toBeNull()
// Team members listed
expect(candidate.teamMembers).toHaveLength(2)
const memberIds = candidate.teamMembers.map((tm: { userId: string }) => tm.userId)
expect(memberIds).toContain(lead.id)
expect(memberIds).toContain(member.id)
const leadMember = candidate.teamMembers.find((tm: { userId: string }) => tm.userId === lead.id)
expect(leadMember!.role).toBe('LEAD')
expect(leadMember!.email).toBeTruthy()
})
it('reflects inLiveFinal: true when the project has a LIVE_FINAL ProjectRoundState', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const program = await createTestProgram({
name: `list-candidates-enrolled-${uid()}`,
defaultAttendeeCap: 3,
})
programIds.push(program.id)
const competition = await createTestCompetition(program.id)
const mentoringRound = await createTestRound(competition.id, {
roundType: 'MENTORING',
sortOrder: 60,
})
const liveFinalRound = await createTestRound(competition.id, {
roundType: 'LIVE_FINAL',
sortOrder: 70,
})
const project = await createTestProject(program.id, {
title: 'Enrolled Candidate',
competitionCategory: 'STARTUP',
})
const lead = await createApplicantUser('LEAD')
userIds.push(lead.id)
await prisma.teamMember.create({
data: { projectId: project.id, userId: lead.id, role: 'LEAD' },
})
// In MENTORING round AND in LIVE_FINAL round
await prisma.projectRoundState.createMany({
data: [
{ projectId: project.id, roundId: mentoringRound.id },
{ projectId: project.id, roundId: liveFinalRound.id },
],
})
const caller = createCaller(finalistRouter, {
id: admin.id,
email: admin.email,
role: 'SUPER_ADMIN',
})
const result = await caller.listEnrollmentCandidates({ programId: program.id })
const startupCat = result.categories.find((c: { category: string }) => c.category === 'STARTUP')
expect(startupCat).toBeDefined()
const candidate = startupCat!.candidates.find((c: { projectId: string }) => c.projectId === project.id)
expect(candidate).toBeDefined()
expect(candidate!.inLiveFinal).toBe(true)
})
it('returns empty categories and null liveFinalRoundId when no MENTORING round exists', async () => {
const admin = await createTestUser('SUPER_ADMIN')
userIds.push(admin.id)
const program = await createTestProgram({ name: `list-candidates-empty-${uid()}` })
programIds.push(program.id)
// Competition with only an EVALUATION round (no MENTORING)
const competition = await createTestCompetition(program.id)
await createTestRound(competition.id, { roundType: 'EVALUATION', sortOrder: 10 })
const caller = createCaller(finalistRouter, {
id: admin.id,
email: admin.email,
role: 'SUPER_ADMIN',
})
const result = await caller.listEnrollmentCandidates({ programId: program.id })
expect(result.categories).toHaveLength(0)
expect(result.liveFinalRoundId).toBeNull()
})
})