MOPC-App/docs/claude-architecture-redesign/11-special-awards.md

966 lines
32 KiB
Markdown

# Special Awards System
## Overview
Special Awards are standalone award tracks that run parallel to the main competition flow. They enable the MOPC platform to recognize excellence in specific areas (e.g., "Innovation Award", "Impact Award", "Youth Leadership Award") with dedicated juries and evaluation processes while referencing the same pool of projects.
### Purpose
Special Awards serve three key purposes:
1. **Parallel Recognition** — Recognize excellence in specific domains beyond the main competition prizes
2. **Specialized Evaluation** — Enable dedicated jury groups with domain expertise to evaluate specific criteria
3. **Flexible Integration** — Awards can piggyback on main rounds or run independently with their own timelines
### Design Philosophy
- **Standalone Entities** — Awards are not tracks; they're first-class entities linked to competitions
- **Two Modes** — STAY_IN_MAIN (piggyback evaluation) or SEPARATE_POOL (independent flow)
- **Dedicated Juries** — Each award can have its own jury group with unique members or shared members
- **Flexible Eligibility** — AI-suggested, manual, round-based, or all-eligible modes
- **Integration with Results** — Award results feed into the confirmation round alongside main competition winners
---
## Current System Analysis
### Current Architecture (Pipeline-Based)
**Current State:**
```
Program
└── Pipeline
├── Track: "Main Competition" (MAIN)
└── Track: "Innovation Award" (AWARD)
├── Stage: "Evaluation" (EVALUATION)
└── Stage: "Results" (RESULTS)
SpecialAward {
id, programId, name, description
trackId → Track (AWARD track)
criteriaText (for AI)
scoringMode: PICK_WINNER | RANKED | SCORED
votingStartAt, votingEndAt
winnerProjectId
useAiEligibility: boolean
}
AwardEligibility { awardId, projectId, eligible, method, aiReasoningJson }
AwardJuror { awardId, userId }
AwardVote { awardId, userId, projectId, rank? }
```
**Current Flow:**
1. Admin creates AWARD track within pipeline
2. Admin configures SpecialAward linked to track
3. Projects routed to award track via ProjectStageState
4. AI or manual eligibility determination
5. Award jurors evaluate/vote
6. Winner selected (admin/award master decision)
**Current Limitations:**
- Awards tied to track concept (being eliminated)
- No distinction between "piggyback" awards and independent awards
- No round-based eligibility
- No jury group integration
- No evaluation form linkage
- No audience voting support
- No integration with confirmation round
---
## Redesigned System: Two Award Modes
### Mode 1: STAY_IN_MAIN
**Concept:** Projects remain in the main competition flow. A dedicated award jury evaluates them using the same submissions, during the same evaluation windows.
**Use Case:** "Innovation Award" — Members of Jury 2 who also serve on the Innovation Award jury score projects specifically for innovation criteria during the Jury 2 evaluation round.
**Characteristics:**
- Projects never leave main track
- Award jury evaluates during specific main evaluation rounds
- Award jury sees the same docs/submissions as main jury
- Award uses its own evaluation form with award-specific criteria
- No separate stages/timeline needed
- Results announced alongside main results
**Data Flow:**
```
Competition → Round 5 (Jury 2 Evaluation)
├─ Main Jury (Jury 2) evaluates with standard criteria
└─ Innovation Award Jury evaluates same projects with innovation criteria
SpecialAward {
evaluationMode: "STAY_IN_MAIN"
evaluationRoundId: "round-5" ← Which main round this award evaluates during
juryGroupId: "innovation-jury" ← Dedicated jury
evaluationFormId: "innovation-form" ← Award-specific criteria
}
```
### Mode 2: SEPARATE_POOL
**Concept:** Dedicated evaluation with separate criteria, submission requirements, and timeline. Projects may be pulled out for award-specific evaluation.
**Use Case:** "Community Impact Award" — Separate jury evaluates finalists specifically for community impact using a unique rubric and potentially additional documentation.
**Characteristics:**
- Own jury group with unique members
- Own evaluation criteria/form
- Can have own submission requirements
- Runs on its own timeline
- Can pull projects from specific rounds
- Independent results timeline
**Data Flow:**
```
Competition
└── SpecialAward {
evaluationMode: "SEPARATE_POOL"
eligibilityMode: "ROUND_BASED" ← Projects from Round 5 (finalists)
juryGroupId: "impact-jury"
evaluationFormId: "impact-form"
votingStartAt: [own window]
votingEndAt: [own window]
}
```
---
## Enhanced SpecialAward Model
### Complete Schema
```prisma
model SpecialAward {
id String @id @default(cuid())
competitionId String // CHANGED: Links to Competition, not Track
name String
description String? @db.Text
// Eligibility configuration
eligibilityMode AwardEligibilityMode @default(AI_SUGGESTED)
eligibilityCriteria Json? @db.JsonB // Mode-specific config
// Evaluation configuration
evaluationMode AwardEvaluationMode @default(STAY_IN_MAIN)
evaluationRoundId String? // Which main round (for STAY_IN_MAIN)
evaluationFormId String? // Custom criteria
juryGroupId String? // Dedicated or shared jury
// Voting configuration
votingMode AwardVotingMode @default(JURY_ONLY)
scoringMode AwardScoringMode @default(PICK_WINNER)
maxRankedPicks Int? // For RANKED mode
maxWinners Int @default(1) // Number of winners
audienceVotingWeight Float? // 0.0-1.0 for COMBINED mode
// Timing
votingStartAt DateTime?
votingEndAt DateTime?
// Results
status AwardStatus @default(DRAFT)
winnerProjectId String? // Single winner (for backward compat)
// AI eligibility
useAiEligibility Boolean @default(false)
criteriaText String? @db.Text // Plain-language for AI
// Job tracking (for AI eligibility)
eligibilityJobStatus String?
eligibilityJobTotal Int?
eligibilityJobDone Int?
eligibilityJobError String? @db.Text
eligibilityJobStarted DateTime?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
evaluationRound Round? @relation("AwardEvaluationRound", fields: [evaluationRoundId], references: [id], onDelete: SetNull)
evaluationForm EvaluationForm? @relation(fields: [evaluationFormId], references: [id], onDelete: SetNull)
juryGroup JuryGroup? @relation("AwardJuryGroup", fields: [juryGroupId], references: [id], onDelete: SetNull)
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
eligibilities AwardEligibility[]
votes AwardVote[]
winners AwardWinner[] // NEW: Multi-winner support
@@index([competitionId])
@@index([status])
@@index([evaluationRoundId])
@@index([juryGroupId])
}
enum AwardEligibilityMode {
AI_SUGGESTED // AI analyzes and suggests eligible projects
MANUAL // Admin manually selects eligible projects
ALL_ELIGIBLE // All projects in competition are eligible
ROUND_BASED // All projects that reach a specific round
}
enum AwardEvaluationMode {
STAY_IN_MAIN // Evaluate during main competition round
SEPARATE_POOL // Independent evaluation flow
}
enum AwardVotingMode {
JURY_ONLY // Only jury votes
AUDIENCE_ONLY // Only audience votes
COMBINED // Jury + audience with weighted scoring
}
enum AwardScoringMode {
PICK_WINNER // Simple winner selection (1 or N winners)
RANKED // Ranked-choice voting
SCORED // Criteria-based scoring
}
enum AwardStatus {
DRAFT
NOMINATIONS_OPEN
EVALUATION // NEW: Award jury evaluation in progress
DECIDED // NEW: Winner(s) selected, pending announcement
ANNOUNCED // NEW: Winner(s) publicly announced
ARCHIVED
}
```
### New Model: AwardWinner (Multi-Winner Support)
```prisma
model AwardWinner {
id String @id @default(cuid())
awardId String
projectId String
rank Int // 1st place, 2nd place, etc.
// Selection metadata
selectedAt DateTime @default(now())
selectedById String
selectionMethod String // "JURY_VOTE" | "AUDIENCE_VOTE" | "COMBINED" | "ADMIN_DECISION"
// Score breakdown (for transparency)
juryScore Float?
audienceScore Float?
finalScore Float?
createdAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
project Project @relation("AwardWinners", fields: [projectId], references: [id], onDelete: Cascade)
selectedBy User @relation("AwardWinnerSelector", fields: [selectedById], references: [id])
@@unique([awardId, projectId])
@@unique([awardId, rank])
@@index([awardId])
@@index([projectId])
}
```
### Enhanced AwardVote Model
```prisma
model AwardVote {
id String @id @default(cuid())
awardId String
userId String? // Nullable for audience votes
projectId String
// Voting type
isAudienceVote Boolean @default(false)
// Scoring (mode-dependent)
rank Int? // For RANKED mode (1 = first choice)
score Float? // For SCORED mode
// Criteria scores (for SCORED mode)
criterionScoresJson Json? @db.JsonB
votedAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([awardId, userId, projectId])
@@index([awardId])
@@index([userId])
@@index([projectId])
@@index([awardId, isAudienceVote])
}
```
---
## Eligibility System Deep Dive
### Eligibility Modes
#### 1. AI_SUGGESTED
AI analyzes all projects and suggests eligible ones based on plain-language criteria.
**Config JSON:**
```typescript
type AISuggestedConfig = {
criteriaText: string // "Projects using innovative ocean tech"
confidenceThreshold: number // 0.0-1.0 (default: 0.7)
autoAcceptAbove: number // Auto-accept above this (default: 0.9)
requireManualReview: boolean // All need admin review (default: false)
sourceRoundId?: string // Only projects from this round
}
```
**Flow:**
1. Admin triggers AI eligibility analysis
2. AI processes projects in batches (anonymized)
3. AI returns: `{ projectId, eligible, confidence, reasoning }`
4. High-confidence results auto-applied
5. Medium-confidence results flagged for review
6. Low-confidence results rejected (or flagged if `requireManualReview: true`)
**UI:**
```
┌─────────────────────────────────────────────────────────────┐
│ Innovation Award — AI Eligibility Analysis │
├─────────────────────────────────────────────────────────────┤
│ Status: Running... (47/120 projects analyzed) │
│ [████████████████░░░░░░░░] 68% │
│ │
│ Results So Far: │
│ ✓ Auto-Accepted (confidence > 0.9): 12 projects │
│ ⚠ Flagged for Review (0.6-0.9): 23 projects │
│ ✗ Rejected (< 0.6): 12 projects │
│ │
│ [View Flagged Projects] [Stop Analysis] │
└─────────────────────────────────────────────────────────────┘
```
#### 2. MANUAL
Admin manually selects eligible projects.
**Config JSON:**
```typescript
type ManualConfig = {
sourceRoundId?: string // Limit to projects from specific round
categoryFilter?: "STARTUP" | "BUSINESS_CONCEPT"
tagFilters?: string[] // Only projects with these tags
}
```
#### 3. ALL_ELIGIBLE
All projects in the competition are automatically eligible.
**Config JSON:**
```typescript
type AllEligibleConfig = {
minimumStatus?: ProjectStatus // e.g., "SEMIFINALIST" or above
excludeWithdrawn: boolean // Exclude WITHDRAWN (default: true)
}
```
#### 4. ROUND_BASED
All projects that reach a specific round are automatically eligible.
**Config JSON:**
```typescript
type RoundBasedConfig = {
sourceRoundId: string // Required: which round
requiredState: ProjectRoundStateValue // PASSED, COMPLETED, etc.
autoUpdate: boolean // Auto-update when projects advance (default: true)
}
```
**Example:**
```json
{
"sourceRoundId": "round-5-jury-2",
"requiredState": "PASSED",
"autoUpdate": true
}
```
### Admin Override System
**All eligibility modes support admin override:**
```prisma
model AwardEligibility {
id String @id @default(cuid())
awardId String
projectId String
// Original determination
method EligibilityMethod @default(AUTO) // AUTO, AI, MANUAL
eligible Boolean @default(false)
aiReasoningJson Json? @db.JsonB
// Override
overriddenBy String?
overriddenAt DateTime?
overrideReason String? @db.Text
// Final decision
finalEligible Boolean // Computed: overridden ? override : original
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id])
@@unique([awardId, projectId])
@@index([awardId, eligible])
@@index([awardId, finalEligible])
}
```
---
## Award Jury Groups
### Integration with JuryGroup Model
Awards can have:
1. **Dedicated Jury** — Own `JuryGroup` with unique members
2. **Shared Jury** — Reuse existing competition jury group (e.g., Jury 2)
3. **Mixed Jury** — Some overlap with main jury, some unique members
**Example:**
```typescript
// Dedicated jury for Innovation Award
const innovationJury = await prisma.juryGroup.create({
data: {
competitionId: "comp-2026",
name: "Innovation Award Jury",
slug: "innovation-jury",
description: "Technology and innovation experts",
defaultMaxAssignments: 15,
defaultCapMode: "SOFT",
categoryQuotasEnabled: false,
}
})
// Add members (can overlap with main jury)
await prisma.juryGroupMember.createMany({
data: [
{ juryGroupId: innovationJury.id, userId: "user-tech-1", isLead: true },
{ juryGroupId: innovationJury.id, userId: "user-tech-2" },
{ juryGroupId: innovationJury.id, userId: "jury-2-member-overlap" }, // Also on Jury 2
]
})
// Link to award
await prisma.specialAward.update({
where: { id: awardId },
data: { juryGroupId: innovationJury.id }
})
```
### Award Jury Assignment
#### For STAY_IN_MAIN Mode
Award jury members evaluate the same projects as the main jury, but with award-specific criteria.
**Assignment Creation:**
```typescript
// Main jury assignments (created by round)
Assignment { userId: "jury-2-member-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "jury-2" }
// Award jury assignments (created separately, same round)
Assignment { userId: "innovation-jury-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "innovation-jury" }
```
**Evaluation:**
- Award jury uses `evaluationFormId` linked to award
- Evaluations stored separately (different `assignmentId`)
- Both juries can evaluate same project in same round
#### For SEPARATE_POOL Mode
Award has its own assignment workflow, potentially for a subset of projects.
---
## Award Evaluation Flow
### STAY_IN_MAIN Evaluation
**Timeline:**
```
Round 5: Jury 2 Evaluation (Main)
├─ Opens: 2026-03-01
├─ Main Jury evaluates with standard form
├─ Innovation Award Jury evaluates with innovation form
└─ Closes: 2026-03-15
Award results calculated separately but announced together
```
**Step-by-Step:**
1. **Setup Phase**
- Admin creates `SpecialAward { evaluationMode: "STAY_IN_MAIN", evaluationRoundId: "round-5" }`
- Admin creates award-specific `EvaluationForm` with innovation criteria
- Admin creates `JuryGroup` for Innovation Award
- Admin adds members to jury group
2. **Eligibility Phase**
- Eligibility determined (AI/manual/round-based)
- Only eligible projects evaluated by award jury
3. **Assignment Phase**
- When Round 5 opens, assignments created for award jury
- Each award juror assigned eligible projects
- Award assignments reference same `roundId` as main evaluation
4. **Evaluation Phase**
- Award jurors see projects in their dashboard
- Form shows award-specific criteria
- Evaluations stored with `formId` = innovation form
5. **Results Phase**
- Scores aggregated separately from main jury
- Winner selection (jury vote, admin decision, etc.)
- Results feed into confirmation round
### SEPARATE_POOL Evaluation
**Timeline:**
```
Round 5: Jury 2 Evaluation (Main) — March 1-15
Round 6: Finalist Selection
Impact Award Evaluation (Separate) — March 20 - April 5
├─ Own voting window
├─ Own evaluation form
├─ Impact Award Jury evaluates finalists
└─ Results: April 10
```
---
## Audience Voting for Awards
### Voting Modes
#### JURY_ONLY
Only jury members vote. Standard model.
#### AUDIENCE_ONLY
Only audience (public) votes. No jury involvement.
**Config:**
```typescript
type AudienceOnlyConfig = {
requireIdentification: boolean // Require email/phone (default: false)
votesPerPerson: number // Max votes per person (default: 1)
allowRanking: boolean // Ranked-choice (default: false)
maxChoices?: number // For ranked mode
}
```
#### COMBINED
Jury + audience votes combined with weighted scoring.
**Config:**
```typescript
type CombinedConfig = {
audienceWeight: number // 0.0-1.0 (e.g., 0.3 = 30% audience, 70% jury)
juryWeight: number // 0.0-1.0 (should sum to 1.0)
requireMinimumAudienceVotes: number // Min votes for validity (default: 50)
showAudienceResultsToJury: boolean // Jury sees audience results (default: false)
}
```
**Scoring Calculation:**
```typescript
function calculateCombinedScore(
juryScores: number[],
audienceVoteCount: number,
totalAudienceVotes: number,
config: CombinedConfig
): number {
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
const audiencePercent = audienceVoteCount / totalAudienceVotes
// Normalize jury score to 0-1 (assuming 1-10 scale)
const normalizedJuryScore = juryAvg / 10
const finalScore =
(normalizedJuryScore * config.juryWeight) +
(audiencePercent * config.audienceWeight)
return finalScore
}
```
---
## Admin Experience
### Award Management Dashboard
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ MOPC 2026 — Special Awards [+ New Award] │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Innovation Award [Edit ▼] │ │
│ │ Mode: Stay in Main (Jury 2 Evaluation) • Status: EVALUATION │ │
│ ├───────────────────────────────────────────────────────────────────────┤ │
│ │ Eligible Projects: 18 / 20 finalists │ │
│ │ Jury: Innovation Jury (5 members) │ │
│ │ Evaluations: 72 / 90 (80% complete) │ │
│ │ Voting Closes: March 15, 2026 │ │
│ │ │ │
│ │ [View Eligibility] [View Evaluations] [Select Winner] │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ Community Impact Award [Edit ▼] │ │
│ │ Mode: Separate Pool • Status: DRAFT │ │
│ ├───────────────────────────────────────────────────────────────────────┤ │
│ │ Eligible Projects: Not yet determined (AI pending) │ │
│ │ Jury: Not assigned │ │
│ │ Voting Window: Not set │ │
│ │ │ │
│ │ [Configure Eligibility] [Set Up Jury] [Set Timeline] │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Integration with Main Flow
### Awards Reference Main Competition Projects
Awards don't create their own project pool — they reference existing competition projects.
**Data Relationship:**
```
Competition
├── Projects (shared pool)
│ ├── Project A
│ ├── Project B
│ └── Project C
├── Main Rounds (linear flow)
│ ├── Round 1: Intake
│ ├── Round 5: Jury 2 Evaluation
│ └── Round 7: Live Finals
└── Special Awards (parallel evaluation)
├── Innovation Award
│ └── AwardEligibility { projectId: "A", eligible: true }
│ └── AwardEligibility { projectId: "B", eligible: false }
└── Impact Award
└── AwardEligibility { projectId: "A", eligible: true }
└── AwardEligibility { projectId: "C", eligible: true }
```
### Award Results Feed into Confirmation Round
**Confirmation Round Integration:**
The confirmation round (Round 8) includes:
1. Main competition winners (1st, 2nd, 3rd per category)
2. Special award winners
**WinnerProposal Extension:**
```prisma
model WinnerProposal {
id String @id @default(cuid())
competitionId String
category CompetitionCategory? // Null for award winners
// Main competition or award
proposalType WinnerProposalType @default(MAIN_COMPETITION)
awardId String? // If proposalType = SPECIAL_AWARD
status WinnerProposalStatus @default(PENDING)
rankedProjectIds String[]
// ... rest of fields ...
}
enum WinnerProposalType {
MAIN_COMPETITION // Main 1st/2nd/3rd place
SPECIAL_AWARD // Award winner
}
```
---
## API Changes
### New tRPC Procedures
```typescript
// src/server/routers/award-redesign.ts
export const awardRedesignRouter = router({
/**
* Create a new special award
*/
create: adminProcedure
.input(z.object({
competitionId: z.string(),
name: z.string().min(1).max(255),
description: z.string().optional(),
eligibilityMode: z.enum(['AI_SUGGESTED', 'MANUAL', 'ALL_ELIGIBLE', 'ROUND_BASED']),
evaluationMode: z.enum(['STAY_IN_MAIN', 'SEPARATE_POOL']),
votingMode: z.enum(['JURY_ONLY', 'AUDIENCE_ONLY', 'COMBINED']),
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
maxWinners: z.number().int().min(1).default(1),
}))
.mutation(async ({ ctx, input }) => { /* ... */ }),
/**
* Run eligibility determination
*/
runEligibility: adminProcedure
.input(z.object({ awardId: z.string() }))
.mutation(async ({ ctx, input }) => { /* ... */ }),
/**
* Cast vote (jury or audience)
*/
vote: protectedProcedure
.input(z.object({
awardId: z.string(),
projectId: z.string(),
rank: z.number().int().min(1).optional(),
score: z.number().min(0).max(10).optional(),
}))
.mutation(async ({ ctx, input }) => { /* ... */ }),
/**
* Select winner(s)
*/
selectWinners: adminProcedure
.input(z.object({
awardId: z.string(),
winnerProjectIds: z.array(z.string()).min(1),
selectionMethod: z.enum(['JURY_VOTE', 'AUDIENCE_VOTE', 'COMBINED', 'ADMIN_DECISION']),
}))
.mutation(async ({ ctx, input }) => { /* ... */ }),
})
```
---
## Service Functions
### Award Service Enhancements
```typescript
// src/server/services/award-service.ts
/**
* Run round-based eligibility
*/
export async function runRoundBasedEligibility(
award: SpecialAward,
prisma = getPrisma()
) {
const config = award.eligibilityCriteria as RoundBasedConfig
if (!config.sourceRoundId) {
throw new Error('Round-based eligibility requires sourceRoundId')
}
// Get all projects in the specified round with the required state
const projectRoundStates = await prisma.projectRoundState.findMany({
where: {
roundId: config.sourceRoundId,
state: config.requiredState ?? 'PASSED',
},
select: { projectId: true }
})
// Create/update eligibility records
let created = 0
let updated = 0
for (const prs of projectRoundStates) {
const existing = await prisma.awardEligibility.findUnique({
where: {
awardId_projectId: {
awardId: award.id,
projectId: prs.projectId
}
}
})
if (existing) {
await prisma.awardEligibility.update({
where: { id: existing.id },
data: { eligible: true, method: 'AUTO' }
})
updated++
} else {
await prisma.awardEligibility.create({
data: {
awardId: award.id,
projectId: prs.projectId,
eligible: true,
method: 'AUTO',
}
})
created++
}
}
return { created, updated, total: projectRoundStates.length }
}
/**
* Calculate combined jury + audience score
*/
export function calculateCombinedScore(
juryScores: number[],
audienceVoteCount: number,
totalAudienceVotes: number,
juryWeight: number,
audienceWeight: number
): number {
if (juryScores.length === 0) {
throw new Error('Cannot calculate combined score without jury votes')
}
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
const normalizedJuryScore = juryAvg / 10 // Assume 1-10 scale
const audiencePercent = totalAudienceVotes > 0
? audienceVoteCount / totalAudienceVotes
: 0
const finalScore =
(normalizedJuryScore * juryWeight) +
(audiencePercent * audienceWeight)
return finalScore
}
/**
* Create award jury assignments
*/
export async function createAwardAssignments(
awardId: string,
prisma = getPrisma()
) {
const award = await prisma.specialAward.findUniqueOrThrow({
where: { id: awardId },
include: {
juryGroup: {
include: { members: true }
}
}
})
if (!award.juryGroupId || !award.juryGroup) {
throw new Error('Award must have a jury group to create assignments')
}
const eligibleProjects = await getEligibleProjects(awardId, prisma)
const assignments = []
for (const project of eligibleProjects) {
for (const member of award.juryGroup.members) {
assignments.push({
userId: member.userId,
projectId: project.id,
roundId: award.evaluationRoundId ?? null,
juryGroupId: award.juryGroupId,
method: 'MANUAL' as const,
})
}
}
await prisma.assignment.createMany({
data: assignments,
skipDuplicates: true,
})
return { created: assignments.length }
}
```
---
## Edge Cases
| Scenario | Handling |
|----------|----------|
| **Project eligible for multiple awards** | Allowed — project can win multiple awards |
| **Jury member on both main and award juries** | Allowed — separate assignments, separate evaluations |
| **Award voting ends before main results** | Award winner held until main results finalized, announced together |
| **Award eligibility changes mid-voting** | Admin override can remove eligibility; active votes invalidated |
| **Audience vote spam/fraud** | IP rate limiting, device fingerprinting, email verification, admin review |
| **Tie in award voting** | Admin decision or re-vote (configurable) |
| **Award jury not complete evaluations** | Admin can close voting with partial data or extend deadline |
| **Project withdrawn after eligible** | Eligibility auto-removed; votes invalidated |
| **Award criteria change after eligibility** | Re-run eligibility or grandfather existing eligible projects |
| **No eligible projects for award** | Award status set to DRAFT/ARCHIVED; no voting |
---
## Integration Points
### With Evaluation System
- Awards use `EvaluationForm` for criteria
- Award evaluations stored in `Evaluation` table with `formId` linkage
- Assignment system handles both main and award assignments
### With Jury Groups
- Awards can link to existing `JuryGroup` or have dedicated groups
- Jury members can overlap between main and award juries
- Caps and quotas honored for award assignments
### With Confirmation Round
- Award winners included in `WinnerProposal` system
- Confirmation flow handles both main and award winners
- Approval workflow requires sign-off on all winners
### With Notification System
- Eligibility notifications sent to eligible teams
- Voting reminders sent to award jurors
- Winner announcements coordinated with main results
---
## Summary
The redesigned Special Awards system provides:
1. **Flexibility**: Two modes (STAY_IN_MAIN, SEPARATE_POOL) cover all use cases
2. **Integration**: Deep integration with competition rounds, juries, and results
3. **Autonomy**: Awards can run independently or piggyback on main flow
4. **Transparency**: AI eligibility with admin override, full audit trail
5. **Engagement**: Audience voting support with anti-fraud measures
6. **Scalability**: Support for multiple awards, multiple winners, complex scoring
This architecture eliminates the Track dependency, integrates awards as standalone entities, and provides a robust, flexible system for recognizing excellence across multiple dimensions while maintaining the integrity of the main competition flow.