# Round: Confirmation — Winner Agreement & Result Freezing

## Overview

The **CONFIRMATION** round (Round 8 in the standard 8-step flow) is the final step of the competition. It transforms live-finals scores into official, immutable results through a multi-party digital agreement process.

### Purpose

After Jury 3 completes live scoring in Round 7 (LIVE_FINAL), the platform generates a **WinnerProposal** — a ranked list of projects per category. This proposal must be ratified by the jury members who scored it, confirmed by the admin, and then **frozen** so that official results cannot be tampered with.

### Key Principles

| Principle | Implementation |
|-----------|---------------|
| **Unanimous by default** | Every jury member on the finals jury must individually approve |
| **Admin override available** | Admin can force-majority or outright select winners |
| **Immutable once frozen** | Frozen results cannot be changed — new proposal required |
| **Full audit trail** | Every approval, rejection, override, and freeze is logged |
| **Per-category** | Startups and Concepts can be confirmed independently |
| **Special awards included** | Award winners are part of the confirmation package |

### Flow Summary

```
Live Finals Complete
  │
  ▼
┌─────────────────────────────┐
│  WinnerProposal Generated   │  ← Auto-generated from scores, or admin-created
│  Status: PENDING             │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│  Jury Approval Requests     │  ← Each jury member gets notification
│  Sent to all Jury 3 members │
└─────────────┬───────────────┘
              │
    ┌─────────┼──────────┐
    ▼         ▼          ▼
 Juror A   Juror B    Juror C     ... each reviews & votes
 APPROVE   APPROVE    REJECT
    │         │          │
    └─────────┼──────────┘
              │
              ▼
        ┌───────────┐
        │ All voted? │
        └─────┬─────┘
              │
     ┌────────┴────────┐
     ▼                 ▼
  Unanimous?      Not Unanimous
     │                 │
     ▼                 ▼
  Status:         Admin Override
  APPROVED        Decision Point
     │                 │
     ▼            ┌────┴──────┐
  Auto-freeze     │           │
  (if enabled)    ▼           ▼
              FORCE_MAJORITY  ADMIN_DECISION
              (Accept if     (Admin picks
               majority       winners
               approved)      directly)
                  │              │
                  ▼              ▼
              Status:        Status:
              OVERRIDDEN     OVERRIDDEN
                  │              │
                  └──────┬───────┘
                         ▼
                    Admin Freeze
                    (manual trigger)
                         │
                         ▼
                    Status: FROZEN
                    Results are official
```

---

## Current System

Today, there is **no confirmation step**. The pipeline flow is:

```
INTAKE → FILTER → EVALUATION → SELECTION → LIVE_FINAL → RESULTS
```

The `RESULTS` stage type simply displays computed results from LIVE_FINAL. There is:
- No jury sign-off on results
- No admin confirmation step
- No freeze/lock mechanism
- No override system for disputed results
- No multi-party agreement

**Impact:** Results can theoretically be changed at any time, there's no evidence trail of who agreed to the outcome, and no formal "cementing" of winners.

---

## Redesigned Confirmation Round

### ConfirmationConfig

```typescript
type ConfirmationConfig = {
  // ── Approval Requirements ──────────────────────────────
  requireAllJuryApproval: boolean;    // true = unanimous, false = majority
  juryGroupId: string | null;         // Which jury must approve (default: Jury 3)
  minimumApprovalThreshold?: number;  // For non-unanimous: minimum % (e.g., 0.67 = 2/3)

  // ── Admin Override ──────────────────────────────────────
  adminOverrideEnabled: boolean;      // Admin can force result
  overrideModes: OverrideMode[];      // Available override options

  // ── Freeze Behavior ─────────────────────────────────────
  autoFreezeOnApproval: boolean;      // Lock results immediately when all approve
  requireExplicitFreeze: boolean;     // Even after approval, admin must click "Freeze"

  // ── Per-category ────────────────────────────────────────
  perCategory: boolean;               // Separate proposals per STARTUP vs CONCEPT

  // ── Special Awards ──────────────────────────────────────
  includeSpecialAwards: boolean;      // Include award winners in confirmation

  // ── Deadline ────────────────────────────────────────────
  approvalDeadlineDays: number;       // Days jury has to respond (default: 7)
  reminderSchedule: number[];         // Days before deadline to remind (e.g., [3, 1])

  // ── Notification ────────────────────────────────────────
  notifyOnApproval: boolean;          // Notify admin when each juror approves
  notifyOnRejection: boolean;         // Notify admin immediately on any rejection
  notifyOnFreeze: boolean;            // Notify all parties when results frozen
};

type OverrideMode = 'FORCE_MAJORITY' | 'ADMIN_DECISION';
```

### Field Behavior Reference

| Field | Default | Description |
|-------|---------|-------------|
| `requireAllJuryApproval` | `true` | When true, ALL jury members must approve. When false, only `minimumApprovalThreshold` % needed |
| `juryGroupId` | Finals jury | Links to the JuryGroup that must confirm. Usually the same jury that scored live finals |
| `minimumApprovalThreshold` | `1.0` | Only used when `requireAllJuryApproval=false`. E.g., 0.67 = two-thirds majority |
| `adminOverrideEnabled` | `true` | Whether admin can bypass jury deadlock |
| `overrideModes` | `['FORCE_MAJORITY', 'ADMIN_DECISION']` | FORCE_MAJORITY: accept if >50% approved. ADMIN_DECISION: admin picks winners directly |
| `autoFreezeOnApproval` | `true` | When all approvals received, auto-freeze results |
| `requireExplicitFreeze` | `false` | If true, even after unanimous approval, admin must click "Freeze" to finalize |
| `perCategory` | `true` | Separate WinnerProposal per category |
| `includeSpecialAwards` | `true` | Include special award winners in confirmation package |
| `approvalDeadlineDays` | `7` | Jury has this many days to approve |
| `reminderSchedule` | `[3, 1]` | Reminder emails N days before deadline |

---

## WinnerProposal Lifecycle

### States

```typescript
enum WinnerProposalStatus {
  PENDING     = 'PENDING',     // Waiting for jury approvals
  APPROVED    = 'APPROVED',    // All required approvals received
  REJECTED    = 'REJECTED',    // Failed — needs new proposal or override
  OVERRIDDEN  = 'OVERRIDDEN',  // Admin used override power
  FROZEN      = 'FROZEN',      // Locked — official, immutable results
}
```

### State Transitions

```
                          ┌───────────────┐
                          │    PENDING     │◄──── Created from scores
                          └───────┬───────┘      or admin draft
                                  │
                    ┌─────────────┼─────────────┐
                    ▼             │             ▼
             All approved    Some rejected   Admin override
                    │             │             │
                    ▼             ▼             ▼
              ┌──────────┐ ┌──────────┐ ┌────────────┐
              │ APPROVED │ │ REJECTED │ │ OVERRIDDEN │
              └────┬─────┘ └────┬─────┘ └─────┬──────┘
                   │            │              │
                   │      Create new           │
                   │      proposal or          │
                   │      admin override       │
                   ▼                           ▼
              ┌──────────────────────────────────┐
              │            FROZEN                 │◄── Immutable
              └──────────────────────────────────┘
```

### Validity Rules

| Transition | From | To | Condition |
|------------|------|----|-----------|
| Auto-approve | PENDING | APPROVED | All required approvals received |
| Reject | PENDING | REJECTED | Any jury member rejects (if unanimous required) |
| Override: Force Majority | PENDING/REJECTED | OVERRIDDEN | Admin triggers; >50% approved |
| Override: Admin Decision | PENDING/REJECTED | OVERRIDDEN | Admin provides new rankings |
| Freeze | APPROVED/OVERRIDDEN | FROZEN | Admin triggers (or auto on approval) |
| Revoke freeze | *never* | — | Cannot unfreeze. Must create new proposal |

---

## Proposal Generation

### Automatic Generation

When the LIVE_FINAL round completes (all projects scored), the system auto-generates a WinnerProposal:

```typescript
async function generateWinnerProposal(
  competitionId: string,
  sourceRoundId: string,
  category: CompetitionCategory,
  proposedById: string  // system or admin user ID
): Promise<WinnerProposal> {
  // 1. Fetch all scored projects in this category from the live final round
  const scores = await getAggregatedScores(sourceRoundId, category);

  // 2. Rank by final score (jury weight + audience weight)
  const ranked = scores
    .sort((a, b) => b.finalScore - a.finalScore)
    .map(s => s.projectId);

  // 3. Build selection basis (evidence trail)
  const selectionBasis = {
    method: 'SCORE_RANKING',
    sourceRound: sourceRoundId,
    scoreBreakdown: scores.map(s => ({
      projectId: s.projectId,
      juryScore: s.juryScore,
      audienceScore: s.audienceScore,
      finalScore: s.finalScore,
      rank: s.rank,
    })),
    generatedAt: new Date().toISOString(),
  };

  // 4. Create proposal
  const proposal = await prisma.winnerProposal.create({
    data: {
      competitionId,
      category,
      status: 'PENDING',
      rankedProjectIds: ranked,
      selectionBasis,
      sourceRoundId,
      proposedById,
    },
  });

  // 5. Create approval records for each jury member
  const juryMembers = await getConfirmationJuryMembers(competitionId);
  await prisma.winnerApproval.createMany({
    data: juryMembers.map(member => ({
      winnerProposalId: proposal.id,
      userId: member.userId,
      role: 'JURY_MEMBER',
    })),
  });

  // 6. Send approval request notifications
  await notifyJuryForConfirmation(proposal.id, juryMembers);

  return proposal;
}
```

### Manual Proposal Creation

Admins can also create proposals manually (e.g., after an admin-decision override):

```typescript
async function createManualProposal(input: {
  competitionId: string;
  category: CompetitionCategory;
  rankedProjectIds: string[];
  justification: string;
  adminId: string;
}): Promise<WinnerProposal> {
  return prisma.winnerProposal.create({
    data: {
      competitionId: input.competitionId,
      category: input.category,
      status: 'PENDING',
      rankedProjectIds: input.rankedProjectIds,
      selectionBasis: {
        method: 'ADMIN_MANUAL',
        justification: input.justification,
        createdBy: input.adminId,
        createdAt: new Date().toISOString(),
      },
      sourceRoundId: await getLatestRoundId(input.competitionId),
      proposedById: input.adminId,
    },
  });
}
```

---

## Jury Approval Process

### Approval Flow

When a WinnerProposal is created, each jury member on the confirmation jury receives a notification:

1. **Notification** — Email + in-app alert: "Winner proposal for STARTUP category is ready for your review"
2. **Review** — Jury member opens the confirmation page, sees:
   - Ranked list of winners (1st, 2nd, 3rd...)
   - Score breakdown per project
   - Access to all submitted documents
   - Comments from other jury members (if any have responded)
3. **Decision** — Jury member clicks APPROVE or REJECT
   - If APPROVE: records timestamp, optional comment
   - If REJECT: must provide a reason (required text field)
4. **Progress tracking** — Admin can see real-time progress: "3/5 jury members approved"

### Jury Confirmation UI

```
┌──────────────────────────────────────────────────────────────────┐
│  🏆 Winner Confirmation — STARTUP Category                      │
│                                                                  │
│  Proposal Status: PENDING (2/5 approved)                        │
│  Deadline: February 22, 2026 (5 days remaining)                 │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Proposed Rankings:                                              │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │  🥇 1st Place: OceanClean AI                             │   │
│  │     Final Score: 92.4 (Jury: 94.0 | Audience: 86.0)     │   │
│  │     [View Submissions]  [View Evaluations]               │   │
│  ├──────────────────────────────────────────────────────────┤   │
│  │  🥈 2nd Place: DeepReef Monitoring                       │   │
│  │     Final Score: 88.7 (Jury: 87.0 | Audience: 95.0)     │   │
│  │     [View Submissions]  [View Evaluations]               │   │
│  ├──────────────────────────────────────────────────────────┤   │
│  │  🥉 3rd Place: CoralGuard                                │   │
│  │     Final Score: 85.1 (Jury: 86.5 | Audience: 79.0)     │   │
│  │     [View Submissions]  [View Evaluations]               │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  Score Methodology: Jury 70% / Audience 30%                     │
│  Source: Round 7 — Live Finals (completed Feb 15, 2026)         │
│                                                                  │
│  ── Your Decision ──────────────────────────────────────────    │
│                                                                  │
│  Comments (optional for approve, required for reject):          │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                                                          │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  [ ✓ Approve Rankings ]    [ ✗ Reject — Request Revision ]      │
│                                                                  │
│  ── Other Jury Responses ───────────────────────────────────    │
│  ✓ Juror A — Approved (Feb 16)                                  │
│  ✓ Juror B — Approved (Feb 16) "Great selections"               │
│  ○ Juror C — Pending                                            │
│  ○ Juror D — Pending                                            │
│  ○ Juror E — Pending                                            │
└──────────────────────────────────────────────────────────────────┘
```

### Approval Logic

```typescript
async function processApproval(
  proposalId: string,
  userId: string,
  approved: boolean,
  comments?: string
): Promise<{ proposal: WinnerProposal; isComplete: boolean }> {
  // 1. Record the approval/rejection
  await prisma.winnerApproval.update({
    where: { winnerProposalId_userId: { winnerProposalId: proposalId, userId } },
    data: {
      approved,
      comments,
      respondedAt: new Date(),
    },
  });

  // 2. Log to audit trail
  await prisma.decisionAuditLog.create({
    data: {
      entityType: 'WINNER_PROPOSAL',
      entityId: proposalId,
      action: approved ? 'JURY_APPROVED' : 'JURY_REJECTED',
      userId,
      details: { comments },
    },
  });

  // 3. Check if all required approvals are in
  const config = await getConfirmationConfig(proposalId);
  const allApprovals = await prisma.winnerApproval.findMany({
    where: { winnerProposalId: proposalId },
  });

  const responded = allApprovals.filter(a => a.respondedAt !== null);
  const approvedCount = responded.filter(a => a.approved === true).length;
  const rejectedCount = responded.filter(a => a.approved === false).length;
  const totalRequired = allApprovals.length;

  let isComplete = false;
  let newStatus: WinnerProposalStatus | null = null;

  if (config.requireAllJuryApproval) {
    // Unanimous mode
    if (rejectedCount > 0) {
      newStatus = 'REJECTED';
      isComplete = true;
    } else if (approvedCount === totalRequired) {
      newStatus = 'APPROVED';
      isComplete = true;
    }
  } else {
    // Threshold mode
    const threshold = config.minimumApprovalThreshold ?? 0.5;
    if (responded.length === totalRequired) {
      if (approvedCount / totalRequired >= threshold) {
        newStatus = 'APPROVED';
      } else {
        newStatus = 'REJECTED';
      }
      isComplete = true;
    }
  }

  let proposal: WinnerProposal;
  if (newStatus) {
    proposal = await prisma.winnerProposal.update({
      where: { id: proposalId },
      data: { status: newStatus },
    });

    // Auto-freeze if configured and approved
    if (newStatus === 'APPROVED' && config.autoFreezeOnApproval) {
      proposal = await freezeProposal(proposalId, 'SYSTEM');
    }

    // Notify admin of completion
    await notifyAdminProposalComplete(proposalId, newStatus);
  } else {
    proposal = await prisma.winnerProposal.findUniqueOrThrow({
      where: { id: proposalId },
    });
    // Notify admin of progress
    if (config.notifyOnApproval && approved) {
      await notifyAdminApprovalProgress(proposalId, approvedCount, totalRequired);
    }
    if (config.notifyOnRejection && !approved) {
      await notifyAdminRejection(proposalId, userId, comments);
    }
  }

  return { proposal, isComplete };
}
```

---

## Admin Override System

The admin override exists for when jury consensus fails or when extraordinary circumstances require admin intervention.

### Override Modes

#### FORCE_MAJORITY

Accept the proposal if more than 50% of jury members approved, even though unanimous approval was required.

```typescript
async function overrideForceMajority(
  proposalId: string,
  adminId: string,
  reason: string
): Promise<WinnerProposal> {
  const approvals = await prisma.winnerApproval.findMany({
    where: { winnerProposalId: proposalId },
  });

  const approvedCount = approvals.filter(a => a.approved === true).length;
  const total = approvals.length;

  if (approvedCount <= total / 2) {
    throw new TRPCError({
      code: 'BAD_REQUEST',
      message: `Cannot force majority: only ${approvedCount}/${total} approved (need >50%)`,
    });
  }

  const proposal = await prisma.winnerProposal.update({
    where: { id: proposalId },
    data: {
      status: 'OVERRIDDEN',
      overrideById: adminId,
      overrideReason: reason,
      overrideAt: new Date(),
    },
  });

  await prisma.decisionAuditLog.create({
    data: {
      entityType: 'WINNER_PROPOSAL',
      entityId: proposalId,
      action: 'ADMIN_FORCE_MAJORITY',
      userId: adminId,
      details: {
        reason,
        approvedCount,
        totalJurors: total,
        rejections: approvals
          .filter(a => a.approved === false)
          .map(a => ({ userId: a.userId, comments: a.comments })),
      },
    },
  });

  return proposal;
}
```

#### ADMIN_DECISION

Admin directly selects winners, bypassing jury vote entirely. This is the nuclear option for deadlocked juries or exceptional circumstances.

```typescript
async function overrideAdminDecision(
  proposalId: string,
  adminId: string,
  newRankedProjectIds: string[],
  reason: string
): Promise<WinnerProposal> {
  // Store original rankings for audit
  const original = await prisma.winnerProposal.findUniqueOrThrow({
    where: { id: proposalId },
  });

  const proposal = await prisma.winnerProposal.update({
    where: { id: proposalId },
    data: {
      status: 'OVERRIDDEN',
      rankedProjectIds: newRankedProjectIds,
      overrideById: adminId,
      overrideReason: reason,
      overrideAt: new Date(),
      selectionBasis: {
        ...((original.selectionBasis as Record<string, unknown>) ?? {}),
        overrideHistory: {
          originalRanking: original.rankedProjectIds,
          newRanking: newRankedProjectIds,
          reason,
          overriddenBy: adminId,
          overriddenAt: new Date().toISOString(),
        },
      },
    },
  });

  await prisma.decisionAuditLog.create({
    data: {
      entityType: 'WINNER_PROPOSAL',
      entityId: proposalId,
      action: 'ADMIN_DECISION_OVERRIDE',
      userId: adminId,
      details: {
        reason,
        originalRanking: original.rankedProjectIds,
        newRanking: newRankedProjectIds,
      },
    },
  });

  return proposal;
}
```

### Admin Override UI

```
┌──────────────────────────────────────────────────────────────────┐
│  ⚠️  Override Winner Proposal — STARTUP Category                │
│                                                                  │
│  Current Status: REJECTED                                        │
│  Jury Votes: 3 Approved / 2 Rejected                            │
│                                                                  │
│  ── Override Options ───────────────────────────────────────     │
│                                                                  │
│  ○ Force Majority                                                │
│    Accept current rankings since 3/5 (60%) approved.             │
│    Rankings remain as originally proposed.                       │
│                                                                  │
│  ○ Admin Decision                                                │
│    Override rankings entirely. You will set the final order.     │
│                                                                  │
│  ── Override Reason (required) ─────────────────────────────    │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │ Two jurors had scheduling conflicts and could not review  │   │
│  │ all presentations. Accepting majority recommendation.     │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ⚠ This action is logged and cannot be undone.                  │
│                                                                  │
│  [ Cancel ]                     [ Apply Override ]               │
└──────────────────────────────────────────────────────────────────┘
```

### Admin Decision — Reorder UI

When ADMIN_DECISION mode is selected:

```
┌──────────────────────────────────────────────────────────────────┐
│  Admin Decision — Set Final Rankings                             │
│                                                                  │
│  Drag to reorder. This becomes the official result.             │
│                                                                  │
│  ┌────┬──────────────────────────────┬──────────┬────────────┐  │
│  │ #  │ Project                      │ Score    │ Actions    │  │
│  ├────┼──────────────────────────────┼──────────┼────────────┤  │
│  │ 1  │ ≡ OceanClean AI              │ 92.4     │ ↑ ↓       │  │
│  │ 2  │ ≡ DeepReef Monitoring        │ 88.7     │ ↑ ↓       │  │
│  │ 3  │ ≡ CoralGuard                 │ 85.1     │ ↑ ↓       │  │
│  │ 4  │ ≡ WaveEnergy Solutions       │ 82.3     │ ↑ ↓       │  │
│  │ 5  │ ≡ PlasticHarvest            │ 79.8     │ ↑ ↓       │  │
│  └────┴──────────────────────────────┴──────────┴────────────┘  │
│                                                                  │
│  Why are you overriding the score-based ranking?                │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │                                                          │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  [ Cancel ]                         [ Confirm Final Rankings ]   │
└──────────────────────────────────────────────────────────────────┘
```

---

## Result Freezing

### What Freeze Does

Freezing a WinnerProposal makes it **immutable** — the official, permanent record of the competition results.

```typescript
async function freezeProposal(
  proposalId: string,
  triggeredBy: string  // userId or 'SYSTEM' for auto-freeze
): Promise<WinnerProposal> {
  const proposal = await prisma.winnerProposal.findUniqueOrThrow({
    where: { id: proposalId },
  });

  if (proposal.status !== 'APPROVED' && proposal.status !== 'OVERRIDDEN') {
    throw new TRPCError({
      code: 'BAD_REQUEST',
      message: `Cannot freeze proposal in ${proposal.status} status. Must be APPROVED or OVERRIDDEN.`,
    });
  }

  const frozen = await prisma.winnerProposal.update({
    where: { id: proposalId },
    data: {
      status: 'FROZEN',
      frozenById: triggeredBy === 'SYSTEM' ? null : triggeredBy,
      frozenAt: new Date(),
    },
  });

  // Log the freeze
  await prisma.decisionAuditLog.create({
    data: {
      entityType: 'WINNER_PROPOSAL',
      entityId: proposalId,
      action: 'RESULTS_FROZEN',
      userId: triggeredBy === 'SYSTEM' ? undefined : triggeredBy,
      details: {
        category: proposal.category,
        rankedProjectIds: proposal.rankedProjectIds,
        frozenAt: new Date().toISOString(),
        method: triggeredBy === 'SYSTEM' ? 'AUTO_FREEZE' : 'MANUAL_FREEZE',
      },
    },
  });

  // Update round status to COMPLETED
  const config = await getConfirmationConfig(proposalId);
  if (config.perCategory) {
    // Check if ALL categories are frozen before completing the round
    const allProposals = await prisma.winnerProposal.findMany({
      where: { competitionId: proposal.competitionId },
    });
    const allFrozen = allProposals.every(p => p.status === 'FROZEN');
    if (allFrozen) {
      await completeConfirmationRound(proposal.competitionId);
    }
  } else {
    await completeConfirmationRound(proposal.competitionId);
  }

  // Send freeze notifications
  await notifyResultsFrozen(proposalId);

  return frozen;
}
```

### Freeze Immutability

Once frozen, a WinnerProposal **cannot be modified**. The following are enforced:

| Operation | Allowed on FROZEN? | Alternative |
|-----------|--------------------|-------------|
| Change rankings | No | Create a new WinnerProposal |
| Change status | No | — |
| Add approvals | No | — |
| Delete proposal | No | — |
| Create new proposal | Yes | New proposal supersedes (admin must mark old as "superseded" in notes) |

### Freeze Guard Middleware

```typescript
// Applied to all mutation procedures on WinnerProposal
function assertNotFrozen(proposalId: string) {
  const proposal = await prisma.winnerProposal.findUniqueOrThrow({
    where: { id: proposalId },
  });
  if (proposal.status === 'FROZEN') {
    throw new TRPCError({
      code: 'FORBIDDEN',
      message: 'This winner proposal is frozen and cannot be modified. Create a new proposal if changes are needed.',
    });
  }
}
```

---

## Special Awards in Confirmation

When `includeSpecialAwards: true`, the confirmation round also covers special award winners.

### Special Award Winner Proposals

Each special award generates its own mini-proposal, but the confirmation process bundles them together:

```typescript
type ConfirmationPackage = {
  // Main competition winners
  mainProposals: {
    startup: WinnerProposal;
    concept: WinnerProposal;
  };

  // Special award winners
  awardProposals: {
    awardId: string;
    awardName: string;
    winnerId: string;  // project ID
    method: 'JURY_VOTE' | 'AUDIENCE_VOTE' | 'COMBINED_VOTE';
    score: number;
  }[];
};
```

### Confirmation UI with Awards

```
┌──────────────────────────────────────────────────────────────────┐
│  Winner Confirmation — Full Package                              │
│                                                                  │
│  ═══ STARTUP Category ════════════════════════════════════════   │
│  Status: APPROVED ✓  [Freeze Results]                           │
│  1st: OceanClean AI (92.4)                                      │
│  2nd: DeepReef Monitoring (88.7)                                │
│  3rd: CoralGuard (85.1)                                         │
│                                                                  │
│  ═══ CONCEPT Category ════════════════════════════════════════   │
│  Status: PENDING (4/5 approved)                                  │
│  1st: BlueTide Analytics (89.2)                                 │
│  2nd: MarineData Hub (84.6)                                     │
│  3rd: SeaWatch Network (81.3)                                   │
│                                                                  │
│  ═══ Special Awards ══════════════════════════════════════════   │
│  Innovation Award: OceanClean AI — Jury Vote (Score: 9.2)      │
│  Impact Award: BlueTide Analytics — Combined (Score: 8.8)       │
│  Community Award: SeaWatch Network — Audience Vote              │
│                                                                  │
│  ── Actions ─────────────────────────────────────────────────   │
│  [ Freeze All Approved ]  [ Override Pending ]  [ Export PDF ]  │
└──────────────────────────────────────────────────────────────────┘
```

---

## Admin Confirmation Dashboard

### Full Dashboard Layout

```
┌──────────────────────────────────────────────────────────────────┐
│  MOPC 2026 — Winner Confirmation                                 │
│  Round 8 of 8 | Status: ACTIVE                                   │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  Progress Summary                                         │  │
│  │  ┌──────────┬──────────┬──────────┬───────────────────┐  │  │
│  │  │ Category │ Status   │ Approvals│ Deadline          │  │  │
│  │  ├──────────┼──────────┼──────────┼───────────────────┤  │  │
│  │  │ STARTUP  │ APPROVED │ 5/5 ✓    │ — (complete)      │  │  │
│  │  │ CONCEPT  │ PENDING  │ 4/5      │ Feb 22 (5 days)   │  │  │
│  │  └──────────┴──────────┴──────────┴───────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  Jury Approval Timeline                                   │  │
│  │                                                           │  │
│  │  Feb 15  ●────────────────────────────── Proposal sent   │  │
│  │  Feb 16  ●── Juror A approved ✓                          │  │
│  │  Feb 16  ●── Juror B approved ✓                          │  │
│  │  Feb 17  ●── Juror C approved ✓                          │  │
│  │  Feb 17  ●── Juror D approved ✓                          │  │
│  │  Feb ??  ○── Juror E — awaiting response                 │  │
│  │                                                           │  │
│  │  [Send Reminder to Juror E]                               │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  Audit Trail                                              │  │
│  │  Feb 17 14:32 — Juror D approved CONCEPT proposal        │  │
│  │  Feb 17 14:30 — Juror D approved STARTUP proposal        │  │
│  │  Feb 17 10:15 — Juror C approved CONCEPT proposal        │  │
│  │  Feb 16 09:22 — Juror B approved with comment            │  │
│  │  Feb 16 09:10 — Juror A approved both categories         │  │
│  │  Feb 15 18:00 — Proposals auto-generated from Round 7    │  │
│  │  [View Full Audit Log]                                    │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                  │
│  Actions:                                                        │
│  [ Freeze STARTUP Results ]  [ Override CONCEPT ]                │
│  [ Send Reminder ]  [ Export Results PDF ]  [ Create New Proposal]│
└──────────────────────────────────────────────────────────────────┘
```

---

## Notification Flow

### Notification Events

| Event | Recipients | Channel | Timing |
|-------|-----------|---------|--------|
| Proposal created | All confirmation jury | Email + In-app | Immediate |
| Jury member approves | Admin | In-app | Immediate |
| Jury member rejects | Admin | Email + In-app | Immediate |
| All approved (unanimous) | Admin + All jury | Email | Immediate |
| Proposal rejected (failed) | Admin | Email + In-app | Immediate |
| Admin override applied | All jury | Email | Immediate |
| Results frozen | All jury + Admin | Email + In-app | Immediate |
| Approval deadline approaching | Pending jury members | Email | Per reminderSchedule |
| Approval deadline passed | Admin | Email + In-app | Immediate |

### Notification Templates

```typescript
const CONFIRMATION_NOTIFICATIONS = {
  PROPOSAL_CREATED: {
    subject: 'MOPC 2026 — Winner Confirmation Required',
    body: `The winner proposal for {{category}} category is ready for your review.
           Please log in and confirm or reject the proposed rankings by {{deadline}}.

           Proposed Winners:
           {{#each rankedProjects}}
             {{rank}}. {{name}} (Score: {{score}})
           {{/each}}`,
  },

  APPROVAL_RECEIVED: {
    subject: 'MOPC 2026 — Confirmation Progress Update',
    body: `{{jurorName}} has {{approved ? "approved" : "rejected"}} the {{category}} proposal.
           Progress: {{approvedCount}}/{{totalJurors}} approved.
           {{#if comments}}Comment: "{{comments}}"{{/if}}`,
  },

  RESULTS_FROZEN: {
    subject: 'MOPC 2026 — Official Results Confirmed',
    body: `The {{category}} category results have been officially confirmed and frozen.

           Official Winners:
           {{#each rankedProjects}}
             {{rank}}. {{name}}
           {{/each}}

           These results are now immutable and represent the official outcome.`,
  },

  DEADLINE_REMINDER: {
    subject: 'MOPC 2026 — Confirmation Deadline in {{daysRemaining}} days',
    body: `You have not yet responded to the {{category}} winner proposal.
           Please review and respond by {{deadline}}.

           [Review Proposal →]`,
  },
};
```

---

## API Changes

### New tRPC Procedures

```typescript
// src/server/routers/winner-confirmation.ts

export const winnerConfirmationRouter = router({
  // ── Proposals ──────────────────────────────────────────

  /** Generate proposals from live final scores */
  generateProposals: adminProcedure
    .input(z.object({
      competitionId: z.string(),
      sourceRoundId: z.string(),
    }))
    .mutation(async ({ input, ctx }) => { ... }),

  /** Create manual proposal (admin override scenario) */
  createManualProposal: adminProcedure
    .input(z.object({
      competitionId: z.string(),
      category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']),
      rankedProjectIds: z.array(z.string()).min(1),
      justification: z.string().min(10),
    }))
    .mutation(async ({ input, ctx }) => { ... }),

  /** Get proposal with approvals */
  getProposal: protectedProcedure
    .input(z.object({ proposalId: z.string() }))
    .query(async ({ input }) => { ... }),

  /** List all proposals for a competition */
  listProposals: protectedProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ input }) => { ... }),

  /** Get the full confirmation package (main + awards) */
  getConfirmationPackage: adminProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ input }) => { ... }),

  // ── Approvals ──────────────────────────────────────────

  /** Jury member approves or rejects */
  submitApproval: juryProcedure
    .input(z.object({
      proposalId: z.string(),
      approved: z.boolean(),
      comments: z.string().optional(),
    }))
    .mutation(async ({ input, ctx }) => { ... }),

  /** Get my pending approvals */
  getMyPendingApprovals: juryProcedure
    .query(async ({ ctx }) => { ... }),

  /** Get approval progress for a proposal */
  getApprovalProgress: protectedProcedure
    .input(z.object({ proposalId: z.string() }))
    .query(async ({ input }) => { ... }),

  // ── Admin Overrides ────────────────────────────────────

  /** Force majority acceptance */
  overrideForceMajority: adminProcedure
    .input(z.object({
      proposalId: z.string(),
      reason: z.string().min(10),
    }))
    .mutation(async ({ input, ctx }) => { ... }),

  /** Admin directly sets winners */
  overrideAdminDecision: adminProcedure
    .input(z.object({
      proposalId: z.string(),
      newRankedProjectIds: z.array(z.string()).min(1),
      reason: z.string().min(10),
    }))
    .mutation(async ({ input, ctx }) => { ... }),

  // ── Freeze ─────────────────────────────────────────────

  /** Freeze a proposal (make results official) */
  freezeProposal: adminProcedure
    .input(z.object({ proposalId: z.string() }))
    .mutation(async ({ input, ctx }) => { ... }),

  /** Freeze all approved proposals at once */
  freezeAll: adminProcedure
    .input(z.object({ competitionId: z.string() }))
    .mutation(async ({ input, ctx }) => { ... }),

  // ── Notifications ──────────────────────────────────────

  /** Send reminder to pending jury members */
  sendReminder: adminProcedure
    .input(z.object({
      proposalId: z.string(),
      userIds: z.array(z.string()).optional(), // specific jurors, or all pending
    }))
    .mutation(async ({ input }) => { ... }),

  // ── Export ─────────────────────────────────────────────

  /** Export results as PDF */
  exportResultsPdf: adminProcedure
    .input(z.object({ competitionId: z.string() }))
    .mutation(async ({ input }) => { ... }),

  /** Export results as structured JSON */
  exportResultsJson: adminProcedure
    .input(z.object({ competitionId: z.string() }))
    .query(async ({ input }) => { ... }),
});
```

---

## Service Functions

### winner-confirmation.ts

```typescript
// src/server/services/winner-confirmation.ts

/** Generate proposals for all categories from live final scores */
export async function generateProposalsFromScores(
  competitionId: string,
  sourceRoundId: string,
  triggeredBy: string
): Promise<WinnerProposal[]>;

/** Create a manual proposal with admin-specified rankings */
export async function createManualProposal(
  competitionId: string,
  category: CompetitionCategory,
  rankedProjectIds: string[],
  justification: string,
  adminId: string
): Promise<WinnerProposal>;

/** Process a jury member's approval/rejection */
export async function processApproval(
  proposalId: string,
  userId: string,
  approved: boolean,
  comments?: string
): Promise<{ proposal: WinnerProposal; isComplete: boolean }>;

/** Override with force-majority rule */
export async function overrideForceMajority(
  proposalId: string,
  adminId: string,
  reason: string
): Promise<WinnerProposal>;

/** Override with admin-selected rankings */
export async function overrideAdminDecision(
  proposalId: string,
  adminId: string,
  newRankedProjectIds: string[],
  reason: string
): Promise<WinnerProposal>;

/** Freeze a proposal — makes results immutable */
export async function freezeProposal(
  proposalId: string,
  triggeredBy: string
): Promise<WinnerProposal>;

/** Freeze all approved/overridden proposals for a competition */
export async function freezeAllApproved(
  competitionId: string,
  adminId: string
): Promise<WinnerProposal[]>;

/** Get the full confirmation package (main winners + special awards) */
export async function getConfirmationPackage(
  competitionId: string
): Promise<ConfirmationPackage>;

/** Check if all categories are frozen for a competition */
export async function isFullyFrozen(
  competitionId: string
): Promise<boolean>;

/** Send reminder notifications to pending jury members */
export async function sendApprovalReminder(
  proposalId: string,
  specificUserIds?: string[]
): Promise<void>;

/** Get aggregated scores from the source round */
export async function getAggregatedScores(
  roundId: string,
  category: CompetitionCategory
): Promise<AggregatedScore[]>;

/** Export frozen results as a structured document */
export async function exportResults(
  competitionId: string,
  format: 'json' | 'pdf'
): Promise<Buffer | Record<string, unknown>>;
```

---

## Edge Cases

| Scenario | Handling |
|----------|----------|
| **Jury member doesn't respond by deadline** | Admin notified. Can send reminders or use override |
| **All jurors reject** | Status = REJECTED. Admin must override or create new proposal |
| **Juror tries to change vote after submitting** | Not allowed. Must contact admin to reset vote |
| **Admin resets a juror's vote** | Audit logged. Juror re-notified to vote again |
| **Tie in live finals scores** | Proposal lists tied projects at same rank. Admin resolves via override |
| **Proposal frozen, then error discovered** | Cannot unfreeze. Admin creates new proposal with explanation |
| **Juror is on multiple categories** | Separate approval records per proposal (per-category) |
| **Competition has no special awards** | `includeSpecialAwards` is false; only main proposals |
| **Admin freezes before all categories confirmed** | Allowed — each category freezes independently |
| **Network failure during approval** | Optimistic UI with retry. Server-side idempotency via unique constraint |
| **Juror removed from jury group after proposal sent** | Approval record remains but marked as N/A. Threshold recalculated |
| **Multiple proposals for same category** | Only latest non-frozen proposal is active. Previous ones archived |
| **Live finals not yet complete** | Cannot generate proposal — round must be in COMPLETED status |
| **Admin tries to freeze PENDING proposal** | Blocked — must be APPROVED or OVERRIDDEN first |

---

## Integration Points

### Inbound (from other rounds/systems)

| Source | Data | Purpose |
|--------|------|---------|
| Round 7 (LIVE_FINAL) | Aggregated scores, rankings | Auto-generate WinnerProposal |
| JuryGroup (Jury 3) | Member list | Determine who must approve |
| SpecialAward system | Award winners | Include in confirmation package |
| Competition settings | Category config | Per-category proposals |

### Outbound (to other systems)

| Target | Data | Purpose |
|--------|------|---------|
| DecisionAuditLog | All actions | Full audit trail |
| Notification system | Events | Email + in-app alerts |
| Export service | Frozen results | PDF/JSON export for records |
| Admin dashboard | Progress metrics | Real-time status display |
| Competition status | COMPLETED flag | Mark competition as finished |

### Confirmation → Competition Completion

When all proposals are frozen:

```typescript
async function completeConfirmationRound(competitionId: string): Promise<void> {
  // 1. Mark the confirmation round as COMPLETED
  const confirmationRound = await prisma.round.findFirst({
    where: { competitionId, roundType: 'CONFIRMATION' },
  });

  if (confirmationRound) {
    await prisma.round.update({
      where: { id: confirmationRound.id },
      data: { status: 'ROUND_COMPLETED' },
    });
  }

  // 2. Update competition status
  await prisma.competition.update({
    where: { id: competitionId },
    data: { status: 'CLOSED' },
  });

  // 3. Log competition completion
  await prisma.decisionAuditLog.create({
    data: {
      entityType: 'COMPETITION',
      entityId: competitionId,
      action: 'COMPETITION_COMPLETED',
      details: {
        completedAt: new Date().toISOString(),
        reason: 'All winner proposals frozen',
      },
    },
  });

  // 4. Send competition-complete notifications
  await notifyCompetitionComplete(competitionId);
}
```

---

## Security Considerations

| Concern | Mitigation |
|---------|------------|
| **Jury member votes for wrong proposal** | Proposals are category-specific; UI shows only relevant proposal |
| **Admin forges jury approvals** | Approvals tied to authenticated session; audit log captures userId |
| **Results tampered after freeze** | FROZEN status enforced at database level; no UPDATE allowed |
| **Unauthorized freeze** | Only SUPER_ADMIN and PROGRAM_ADMIN can freeze (adminProcedure) |
| **Replay attack on approval** | Unique constraint on [winnerProposalId, userId] prevents double-voting |
| **Override without justification** | `reason` field is required (min 10 chars) on all overrides |
| **PDF export tampering** | Export includes cryptographic hash of frozen proposal data |

---

## Results Export Format

### JSON Export

```typescript
type ExportedResults = {
  competition: {
    id: string;
    name: string;
    programYear: number;
  };
  exportedAt: string;
  exportedBy: string;

  categories: {
    category: string;
    proposalId: string;
    status: WinnerProposalStatus;
    frozenAt: string;

    winners: {
      rank: number;
      project: {
        id: string;
        name: string;
        teamName: string;
        category: string;
      };
      scores: {
        juryScore: number;
        audienceScore: number;
        finalScore: number;
      };
    }[];

    approvals: {
      jurorName: string;
      approved: boolean;
      respondedAt: string;
      comments?: string;
    }[];

    override?: {
      type: string;
      reason: string;
      overriddenBy: string;
      overriddenAt: string;
    };
  }[];

  specialAwards: {
    awardName: string;
    winnerProject: string;
    method: string;
    score: number;
  }[];

  // Integrity hash
  integrityHash: string;
};
```

### PDF Export Structure

The PDF export contains:
1. **Cover page** — Competition name, year, date of confirmation
2. **Main winners** — Per category: ranked list with scores, team info
3. **Special awards** — Award name, winner, scoring method
4. **Jury confirmations** — List of all approvals with timestamps
5. **Override record** — If any override was used, full details
6. **Audit summary** — Key events from the audit trail
7. **Integrity hash** — SHA-256 of all frozen proposal data
