feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s

- processRoundClose EVALUATION uses ranking scores + advanceMode config
  (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED
- Advancement emails generate invite tokens for passwordless users with
  "Create Your Account" CTA; rejection emails have no link
- Finalization UI shows account stats (invite vs dashboard link counts)
- Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson)
- New award pool notification system: getAwardSelectionNotificationTemplate email,
  notifyEligibleProjects mutation with invite token generation,
  "Notify Pool" button on award detail page with custom message dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 19:14:41 +01:00
parent 7735f3ecdf
commit cfee3bc8a9
48 changed files with 5294 additions and 676 deletions

View File

@@ -2193,6 +2193,11 @@ model Round {
submissionWindowId String?
specialAwardId String?
// Finalization
gracePeriodEndsAt DateTime?
finalizedAt DateTime?
finalizedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -2237,13 +2242,14 @@ model Round {
}
model ProjectRoundState {
id String @id @default(cuid())
projectId String
roundId String
state ProjectRoundStateValue @default(PENDING)
enteredAt DateTime @default(now())
exitedAt DateTime?
metadataJson Json? @db.JsonB
id String @id @default(cuid())
projectId String
roundId String
state ProjectRoundStateValue @default(PENDING)
proposedOutcome ProjectRoundStateValue?
enteredAt DateTime @default(now())
exitedAt DateTime?
metadataJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

View File

@@ -559,7 +559,7 @@ async function main() {
})
// Create project
await prisma.project.create({
const createdProject = await prisma.project.create({
data: {
programId: program.id,
title: projectName || `Project by ${name}`,
@@ -584,13 +584,24 @@ async function main() {
},
})
// Link submitter as team lead
await prisma.teamMember.upsert({
where: { projectId_userId: { projectId: createdProject.id, userId: user.id } },
update: {},
create: {
projectId: createdProject.id,
userId: user.id,
role: 'LEAD',
},
})
projectCount++
if (projectCount % 50 === 0) {
console.log(` ... ${projectCount} projects created`)
}
}
console.log(` ✓ Created ${projectCount} projects`)
console.log(` ✓ Created ${projectCount} projects (with team lead links)`)
if (skippedNoEmail > 0) {
console.log(` ⚠ Skipped ${skippedNoEmail} rows with no valid email`)
}
@@ -864,6 +875,24 @@ async function main() {
}
console.log(`${rounds.length - 1} advancement rules created`)
// --- Assign all projects to intake round (COMPLETED, since intake is closed) ---
const intakeRound = rounds[0]
const allProjects = await prisma.project.findMany({
where: { programId: program.id },
select: { id: true },
})
if (allProjects.length > 0) {
await prisma.projectRoundState.createMany({
data: allProjects.map((p) => ({
projectId: p.id,
roundId: intakeRound.id,
state: 'COMPLETED' as const,
})),
skipDuplicates: true,
})
console.log(`${allProjects.length} projects assigned to intake round (COMPLETED)`)
}
// --- Round-Submission Visibility (which rounds can see which submission windows) ---
// R2 and R3 can see R1 docs, R5 can see R4 docs
const visibilityLinks = [