diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index 0aedf67..d518d8e 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -1396,7 +1396,18 @@ export const userRouter = router({ */ getOnboardingContext: protectedProcedure.query(async ({ ctx }) => { const memberships = await ctx.prisma.juryGroupMember.findMany({ - where: { userId: ctx.user.id }, + where: { + userId: ctx.user.id, + juryGroup: { + rounds: { + some: { + roundType: { + in: ['INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING'], + }, + }, + }, + }, + }, include: { juryGroup: { select: { diff --git a/tests/unit/jury-preferences-filter.test.ts b/tests/unit/jury-preferences-filter.test.ts new file mode 100644 index 0000000..6fcc8c2 --- /dev/null +++ b/tests/unit/jury-preferences-filter.test.ts @@ -0,0 +1,153 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest' +import { prisma, createCaller } from '../setup' +import { + createTestUser, createTestProgram, createTestCompetition, createTestRound, + cleanupTestData, uid, +} from '../helpers' +import { userRouter } from '../../src/server/routers/user' + +describe('user.getOnboardingContext — preferences filter excludes LIVE_FINAL/DELIBERATION-only groups', () => { + let programId: string + let competitionId: string + let juror: { id: string; email: string; role: 'JURY_MEMBER' } + let observerOnlyGroupId: string + let reviewGroupId: string + let mixedGroupId: string + const userIds: string[] = [] + + beforeAll(async () => { + const program = await createTestProgram({ name: `prefs-filter-${uid()}` }) + programId = program.id + const competition = await createTestCompetition(programId) + competitionId = competition.id + + const reviewRound = await createTestRound(competitionId, { + name: 'Review Round', slug: `review-${uid()}`, roundType: 'EVALUATION', sortOrder: 0, + }) + const liveFinalRound = await createTestRound(competitionId, { + name: 'Final Round', slug: `final-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 1, + }) + const deliberationRound = await createTestRound(competitionId, { + name: 'Delib Round', slug: `delib-${uid()}`, roundType: 'DELIBERATION', sortOrder: 2, + }) + + const reviewOnlyGroup = await prisma.juryGroup.create({ + data: { + id: uid('jg-rev'), competitionId, name: 'Review Only Group', + slug: uid('rev'), defaultMaxAssignments: 30, + }, + }) + reviewGroupId = reviewOnlyGroup.id + const liveFinalOnlyGroup = await prisma.juryGroup.create({ + data: { + id: uid('jg-fin'), competitionId, name: 'Finals Only Group', + slug: uid('fin'), defaultMaxAssignments: 10, + }, + }) + observerOnlyGroupId = liveFinalOnlyGroup.id + const mixedGroup = await prisma.juryGroup.create({ + data: { + id: uid('jg-mix'), competitionId, name: 'Mixed Group', + slug: uid('mix'), defaultMaxAssignments: 20, + }, + }) + mixedGroupId = mixedGroup.id + + await prisma.round.update({ where: { id: reviewRound.id }, data: { juryGroupId: reviewOnlyGroup.id } }) + await prisma.round.update({ where: { id: liveFinalRound.id }, data: { juryGroupId: liveFinalOnlyGroup.id } }) + const mixedReview = await createTestRound(competitionId, { + name: 'Mixed Review', slug: `mixed-rev-${uid()}`, roundType: 'EVALUATION', sortOrder: 3, + }) + const mixedFinal = await createTestRound(competitionId, { + name: 'Mixed Final', slug: `mixed-fin-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 4, + }) + await prisma.round.update({ where: { id: mixedReview.id }, data: { juryGroupId: mixedGroup.id } }) + await prisma.round.update({ where: { id: mixedFinal.id }, data: { juryGroupId: mixedGroup.id } }) + + void deliberationRound // referenced for cleanup; not attached to a group in these scenarios + + const u = await createTestUser('JURY_MEMBER') + userIds.push(u.id) + juror = { id: u.id, email: u.email, role: 'JURY_MEMBER' } + + await prisma.juryGroupMember.createMany({ + data: [ + { id: uid('jgm-rev'), juryGroupId: reviewGroupId, userId: u.id, role: 'MEMBER' }, + { id: uid('jgm-fin'), juryGroupId: observerOnlyGroupId, userId: u.id, role: 'MEMBER' }, + { id: uid('jgm-mix'), juryGroupId: mixedGroupId, userId: u.id, role: 'MEMBER' }, + ], + }) + }) + + afterAll(async () => { + await cleanupTestData(programId, userIds) + }) + + it('returns the review-only group membership', async () => { + const caller = createCaller(userRouter, juror) + const ctx = await caller.getOnboardingContext() + const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName).sort() + expect(names).toContain('Review Only Group') + }) + + it('omits the LIVE_FINAL-only group membership', async () => { + const caller = createCaller(userRouter, juror) + const ctx = await caller.getOnboardingContext() + const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName) + expect(names).not.toContain('Finals Only Group') + }) + + it('keeps the mixed group (has at least one review round)', async () => { + const caller = createCaller(userRouter, juror) + const ctx = await caller.getOnboardingContext() + const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName) + expect(names).toContain('Mixed Group') + }) + + it('returns hasSelfServiceOptions=true when at least one membership remains', async () => { + const caller = createCaller(userRouter, juror) + const ctx = await caller.getOnboardingContext() + expect(ctx.hasSelfServiceOptions).toBe(true) + expect(ctx.memberships.length).toBe(2) + }) +}) + +describe('user.getOnboardingContext — juror with only LIVE_FINAL membership', () => { + let programId: string + let juror: { id: string; email: string; role: 'JURY_MEMBER' } + const userIds: string[] = [] + + beforeAll(async () => { + const program = await createTestProgram({ name: `prefs-only-fin-${uid()}` }) + programId = program.id + const competition = await createTestCompetition(programId) + const liveFinalRound = await createTestRound(competition.id, { + name: 'Solo Final', slug: `solo-fin-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 0, + }) + const liveFinalOnlyGroup = await prisma.juryGroup.create({ + data: { + id: uid('jg-only-fin'), competitionId: competition.id, name: 'Solo Finals Group', + slug: uid('solo-fin'), defaultMaxAssignments: 10, + }, + }) + await prisma.round.update({ where: { id: liveFinalRound.id }, data: { juryGroupId: liveFinalOnlyGroup.id } }) + + const u = await createTestUser('JURY_MEMBER') + userIds.push(u.id) + juror = { id: u.id, email: u.email, role: 'JURY_MEMBER' } + await prisma.juryGroupMember.create({ + data: { id: uid('jgm-only-fin'), juryGroupId: liveFinalOnlyGroup.id, userId: u.id, role: 'MEMBER' }, + }) + }) + + afterAll(async () => { + await cleanupTestData(programId, userIds) + }) + + it('returns no memberships and hasSelfServiceOptions=false', async () => { + const caller = createCaller(userRouter, juror) + const ctx = await caller.getOnboardingContext() + expect(ctx.memberships).toEqual([]) + expect(ctx.hasSelfServiceOptions).toBe(false) + }) +})