32 KiB
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:
- Parallel Recognition — Recognize excellence in specific domains beyond the main competition prizes
- Specialized Evaluation — Enable dedicated jury groups with domain expertise to evaluate specific criteria
- 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:
- Admin creates AWARD track within pipeline
- Admin configures SpecialAward linked to track
- Projects routed to award track via ProjectStageState
- AI or manual eligibility determination
- Award jurors evaluate/vote
- 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
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)
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
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:
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:
- Admin triggers AI eligibility analysis
- AI processes projects in batches (anonymized)
- AI returns:
{ projectId, eligible, confidence, reasoning } - High-confidence results auto-applied
- Medium-confidence results flagged for review
- 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:
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:
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:
type RoundBasedConfig = {
sourceRoundId: string // Required: which round
requiredState: ProjectRoundStateValue // PASSED, COMPLETED, etc.
autoUpdate: boolean // Auto-update when projects advance (default: true)
}
Example:
{
"sourceRoundId": "round-5-jury-2",
"requiredState": "PASSED",
"autoUpdate": true
}
Admin Override System
All eligibility modes support admin override:
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:
- Dedicated Jury — Own
JuryGroupwith unique members - Shared Jury — Reuse existing competition jury group (e.g., Jury 2)
- Mixed Jury — Some overlap with main jury, some unique members
Example:
// 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:
// 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
evaluationFormIdlinked 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:
-
Setup Phase
- Admin creates
SpecialAward { evaluationMode: "STAY_IN_MAIN", evaluationRoundId: "round-5" } - Admin creates award-specific
EvaluationFormwith innovation criteria - Admin creates
JuryGroupfor Innovation Award - Admin adds members to jury group
- Admin creates
-
Eligibility Phase
- Eligibility determined (AI/manual/round-based)
- Only eligible projects evaluated by award jury
-
Assignment Phase
- When Round 5 opens, assignments created for award jury
- Each award juror assigned eligible projects
- Award assignments reference same
roundIdas main evaluation
-
Evaluation Phase
- Award jurors see projects in their dashboard
- Form shows award-specific criteria
- Evaluations stored with
formId= innovation form
-
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:
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:
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:
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:
- Main competition winners (1st, 2nd, 3rd per category)
- Special award winners
WinnerProposal Extension:
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
// 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
// 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
EvaluationFormfor criteria - Award evaluations stored in
Evaluationtable withformIdlinkage - Assignment system handles both main and award assignments
With Jury Groups
- Awards can link to existing
JuryGroupor 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
WinnerProposalsystem - 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:
- Flexibility: Two modes (STAY_IN_MAIN, SEPARATE_POOL) cover all use cases
- Integration: Deep integration with competition rounds, juries, and results
- Autonomy: Awards can run independently or piggyback on main flow
- Transparency: AI eligibility with admin override, full audit trail
- Engagement: Audience voting support with anti-fraud measures
- 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.