fix(finalization): skip MENTORING rounds in advancement display copy
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m10s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m10s
The mentoring round is opt-in (eligibility: requested_only) and only a subset of advancing teams enter it; the rest auto-pass through. Showing it as the "next round" in the finalization summary and advancement emails was misleading since Grand Finale is the shared destination for all advancing teams. Routing is unchanged — targetRoundId still points to the next round by sortOrder (may be MENTORING) so opt-in handling is preserved. Only the user-facing label skips MENTORING. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
processRoundClose,
|
||||
getFinalizationSummary,
|
||||
confirmFinalization,
|
||||
findDisplayNextRound,
|
||||
} from '../services/round-finalization'
|
||||
import {
|
||||
getAdvancementNotificationTemplate,
|
||||
@@ -382,14 +383,19 @@ export const roundEngineRouter = router({
|
||||
select: {
|
||||
name: true,
|
||||
competition: {
|
||||
select: { rounds: { select: { id: true, name: true, sortOrder: true }, orderBy: { sortOrder: 'asc' } } },
|
||||
select: {
|
||||
rounds: {
|
||||
select: { id: true, name: true, sortOrder: true, roundType: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const rounds = round.competition.rounds
|
||||
const currentIdx = rounds.findIndex((r) => r.id === input.roundId)
|
||||
const nextRound = rounds[currentIdx + 1]
|
||||
const toRoundName = nextRound?.name ?? 'Next Round'
|
||||
// Skip MENTORING rounds for display — those are opt-in and not the
|
||||
// shared destination for all advancing teams.
|
||||
const toRoundName = findDisplayNextRound(rounds, input.roundId)?.name ?? 'Next Round'
|
||||
|
||||
const passedCount = await ctx.prisma.projectRoundState.count({
|
||||
where: { roundId: input.roundId, proposedOutcome: 'PASSED' },
|
||||
|
||||
@@ -400,6 +400,30 @@ export async function processRoundClose(
|
||||
return { processed }
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Pick the round to *display* as the destination after finalization.
|
||||
*
|
||||
* Mentoring rounds are opt-in (eligibility: "requested_only") — only teams that
|
||||
* requested mentorship enter, and the rest auto-pass through to the round after.
|
||||
* For user-facing copy ("X teams will advance to ..."), we therefore skip past
|
||||
* MENTORING rounds and report the next "proper" round (e.g. Grand Finale)
|
||||
* that all advancing teams ultimately share. The actual targetRoundId used by
|
||||
* the round engine for state transitions is unaffected.
|
||||
*/
|
||||
export function findDisplayNextRound<T extends { id: string; name: string; roundType?: RoundType }>(
|
||||
rounds: readonly T[],
|
||||
currentRoundId: string,
|
||||
): T | null {
|
||||
const currentIdx = rounds.findIndex((r) => r.id === currentRoundId)
|
||||
if (currentIdx < 0) return null
|
||||
for (let i = currentIdx + 1; i < rounds.length; i++) {
|
||||
if (rounds[i].roundType !== 'MENTORING') return rounds[i]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// ─── getFinalizationSummary ─────────────────────────────────────────────────
|
||||
|
||||
export async function getFinalizationSummary(
|
||||
@@ -412,7 +436,7 @@ export async function getFinalizationSummary(
|
||||
competition: {
|
||||
select: {
|
||||
rounds: {
|
||||
select: { id: true, name: true, sortOrder: true },
|
||||
select: { id: true, name: true, sortOrder: true, roundType: true },
|
||||
orderBy: { sortOrder: 'asc' as const },
|
||||
},
|
||||
},
|
||||
@@ -427,12 +451,10 @@ export async function getFinalizationSummary(
|
||||
// Get config for category targets
|
||||
const config = (round.configJson as Record<string, unknown>) ?? {}
|
||||
|
||||
// Find next round
|
||||
// Find next round (skip MENTORING rounds — those are opt-in and not the
|
||||
// shared destination for all advancing teams)
|
||||
const rounds = round.competition.rounds
|
||||
const currentIdx = rounds.findIndex((r: { id: string }) => r.id === roundId)
|
||||
const nextRound = currentIdx >= 0 && currentIdx < rounds.length - 1
|
||||
? rounds[currentIdx + 1]
|
||||
: null
|
||||
const nextRound = findDisplayNextRound(rounds, roundId)
|
||||
|
||||
// Get all project states with project details
|
||||
const projectStates = await prisma.projectRoundState.findMany({
|
||||
@@ -613,7 +635,7 @@ export async function confirmFinalization(
|
||||
select: {
|
||||
id: true,
|
||||
rounds: {
|
||||
select: { id: true, name: true, sortOrder: true },
|
||||
select: { id: true, name: true, sortOrder: true, roundType: true },
|
||||
orderBy: { sortOrder: 'asc' as const },
|
||||
},
|
||||
},
|
||||
@@ -634,7 +656,8 @@ export async function confirmFinalization(
|
||||
throw new Error('Cannot finalize: grace period is still active')
|
||||
}
|
||||
|
||||
// Determine target round
|
||||
// Determine target round for routing: the actual next round by sortOrder
|
||||
// (may be a MENTORING round — opt-in teams stay there, others auto-pass through).
|
||||
const rounds = round.competition.rounds
|
||||
const currentIdx = rounds.findIndex((r: { id: string }) => r.id === roundId)
|
||||
const targetRoundId = options.targetRoundId
|
||||
@@ -642,9 +665,10 @@ export async function confirmFinalization(
|
||||
? rounds[currentIdx + 1].id
|
||||
: undefined)
|
||||
|
||||
const targetRoundName = targetRoundId
|
||||
? rounds.find((r: { id: string }) => r.id === targetRoundId)?.name ?? 'Next Round'
|
||||
: 'Next Round'
|
||||
// Display name for emails / notifications: skip MENTORING rounds so the
|
||||
// copy reflects the shared destination (e.g. "Grand Finale") rather than
|
||||
// the opt-in mentoring step.
|
||||
const targetRoundName = findDisplayNextRound(rounds, roundId)?.name ?? 'Next Round'
|
||||
|
||||
// Execute finalization in a transaction
|
||||
const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
||||
|
||||
Reference in New Issue
Block a user