# API Router Reference: Architecture Redesign ## Overview This document provides a complete reference of all tRPC router changes for the MOPC architecture redesign. Every procedure — new, modified, preserved, and removed — is documented with full Zod schemas. **Key Changes Summary:** - **Renamed Routers**: `pipeline` → `competition`, Stage CRUD split between `competition` and `round` routers - **New Routers**: `jury-group`, `winner-confirmation`, `submission-window`, `mentor-workspace` - **Enhanced Routers**: `assignment`, `evaluation`, `file`, `applicant`, `award`, `live-control`, `mentor` - **Eliminated**: `stageTransition` procedures (replaced by linear advancement rules) - **Procedure Count**: ~180 existing procedures, ~60 new, ~30 modified, ~20 removed --- ## Router Rename Map | Current Router | Redesigned Router | Reason | |----------------|-------------------|--------| | `pipeline.ts` | `competition.ts` | Domain-specific naming | | `stage.ts` | Split: `competition.ts` (CRUD) + `round.ts` (runtime) | Clearer separation of concerns | | `assignment.ts` | `assignment.ts` (enhanced) | Add jury group awareness | | `specialAward.ts` | `award.ts` | Enhanced for two-mode awards | | `mentor.ts` | `mentor.ts` + `mentor-workspace.ts` | Separate workspace features | | *(new)* | `jury-group.ts` | Explicit jury management | | *(new)* | `winner-confirmation.ts` | Multi-party winner approval | | *(new)* | `submission-window.ts` | Multi-round document requirements | --- ## 1. Competition Router (replaces Pipeline) **File:** `src/server/routers/competition.ts` (renamed from `pipeline.ts`) ### Removed Procedures All Track-related procedures are eliminated (Track model is removed): - ~~`pipeline.createStructure`~~ — Replaced by `competition.createStructure` (no tracks) - ~~`pipeline.getDraft`~~ — Replaced by `competition.getDraft` (returns rounds, not tracks) - ~~`pipeline.updateStructure`~~ — Replaced by `competition.updateStructure` (rounds only) - ~~`pipeline.getApplicantView`~~ — Moved to `applicant.getCompetitionView` ### Modified Procedures #### `create` → `competition.create` **Changes:** Rename references, simplified settings (no track concept) ```typescript // Input schema z.object({ programId: z.string(), name: z.string().min(1).max(255), slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/), // NEW: Competition-wide settings (typed, not generic JSON) categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'), startupFinalistCount: z.number().int().min(1).default(3), conceptFinalistCount: z.number().int().min(1).default(3), // Notification preferences notifyOnRoundAdvance: z.boolean().default(true), notifyOnDeadlineApproach: z.boolean().default(true), deadlineReminderDays: z.array(z.number().int()).default([7, 3, 1]), }) // Output: Competition ``` #### `update` → `competition.update` **Changes:** Add typed settings fields ```typescript // Input schema z.object({ id: z.string(), name: z.string().min(1).max(255).optional(), slug: z.string().regex(/^[a-z0-9-]+$/).optional(), status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(), // NEW: Typed settings categoryMode: z.enum(['SHARED', 'SPLIT']).optional(), startupFinalistCount: z.number().int().min(1).optional(), conceptFinalistCount: z.number().int().min(1).optional(), notifyOnRoundAdvance: z.boolean().optional(), notifyOnDeadlineApproach: z.boolean().optional(), deadlineReminderDays: z.array(z.number().int()).optional(), }) // Output: Competition ``` #### `get` → `competition.get` **Changes:** Returns rounds array (flat), not tracks ```typescript // Input schema z.object({ id: z.string() }) // Output { ...competition, program: { id, name }, rounds: Array<{ id, name, slug, roundType, status, sortOrder, windowOpenAt, windowCloseAt, juryGroup: { id, name } | null, submissionWindow: { id, name } | null, _count: { projectRoundStates, cohorts } }>, juryGroups: Array<{ id, name, _count: { members } }>, submissionWindows: Array<{ id, name, roundNumber }>, specialAwards: Array<{ id, name, status }>, } ``` #### `list` → `competition.list` **Changes:** Returns competition counts (no tracks) ```typescript // Input schema z.object({ programId: z.string() }) // Output Array<{ ...competition, _count: { rounds, juryGroups, submissionWindows, specialAwards } }> ``` #### `publish` → `competition.publish` **Changes:** Validates rounds instead of tracks ```typescript // Input schema z.object({ id: z.string() }) // Validation: Must have at least one round, all rounds must have configs // Output: Competition with status = ACTIVE ``` ### New Procedures #### `createStructure` **Purpose:** Atomically create Competition + Rounds + Jury Groups + Submission Windows ```typescript // Input schema z.object({ programId: z.string().min(1), name: z.string().min(1).max(255), slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/), // Competition-wide settings categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'), startupFinalistCount: z.number().int().default(3), conceptFinalistCount: z.number().int().default(3), // Rounds (linear array, no tracks) rounds: z.array(z.object({ name: z.string().min(1).max(255), slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/), roundType: z.enum([ 'INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'CONFIRMATION' ]), sortOrder: z.number().int().min(0), configJson: z.record(z.unknown()).optional(), windowOpenAt: z.date().optional(), windowCloseAt: z.date().optional(), // Links to other entities juryGroupSlug: z.string().optional(), // Will link to JuryGroup after creation submissionWindowSlug: z.string().optional(), })), // Jury groups juryGroups: z.array(z.object({ name: z.string(), slug: z.string(), description: z.string().optional(), sortOrder: z.number().int(), defaultMaxAssignments: z.number().int().default(20), defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'), softCapBuffer: z.number().int().default(2), categoryQuotasEnabled: z.boolean().default(false), defaultCategoryQuotas: z.record(z.object({ min: z.number().int(), max: z.number().int(), })).optional(), })), // Submission windows submissionWindows: z.array(z.object({ name: z.string(), slug: z.string(), roundNumber: z.number().int(), sortOrder: z.number().int(), windowOpenAt: z.date().optional(), windowCloseAt: z.date().optional(), deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'), graceHours: z.number().int().optional(), lockOnClose: z.boolean().default(true), fileRequirements: z.array(z.object({ name: z.string(), description: z.string().optional(), acceptedMimeTypes: z.array(z.string()), maxSizeMB: z.number().int().optional(), isRequired: z.boolean().default(true), sortOrder: z.number().int(), })), })), // Advancement rules (auto-generated linear flow by default) autoAdvancement: z.boolean().default(true), }) // Output { competition: Competition, rounds: Array, juryGroups: Array, submissionWindows: Array, } ``` #### `updateStructure` **Purpose:** Diff-based update (create/update/delete rounds, jury groups, windows) ```typescript // Input schema z.object({ id: z.string(), name: z.string().optional(), slug: z.string().optional(), status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(), // Same nested arrays as createStructure, but with optional `id` fields rounds: z.array(z.object({ id: z.string().optional(), // Present = update, absent = create name: z.string(), // ... all other round fields })), juryGroups: z.array(z.object({ id: z.string().optional(), name: z.string(), // ... all other jury group fields })), submissionWindows: z.array(z.object({ id: z.string().optional(), name: z.string(), // ... all other submission window fields })), }) // Logic: // - Rounds/groups/windows with ID → update // - Rounds/groups/windows without ID → create // - Existing IDs not in input → delete (with safety checks: no active ProjectRoundStates) ``` #### `getDraft` **Purpose:** Get full structure for wizard editing ```typescript // Input schema z.object({ id: z.string() }) // Output { ...competition, program: { id, name }, rounds: Array<{ ...round, juryGroup: JuryGroup | null, submissionWindow: SubmissionWindow | null, visibleSubmissionWindows: Array, advancementRules: Array, _count: { projectRoundStates }, }>, juryGroups: Array<{ ...juryGroup, members: Array, _count: { rounds, assignments }, }>, submissionWindows: Array<{ ...submissionWindow, fileRequirements: Array, rounds: Array<{ id, name }>, }>, specialAwards: Array<{ ...award, juryGroup: { id, name } | null, evaluationRound: { id, name } | null, }>, } ``` #### `simulate` **Purpose:** Dry-run project routing through competition ```typescript // Input schema z.object({ id: z.string(), projectIds: z.array(z.string()).min(1).max(500), }) // Output { competitionId: string, competitionName: string, projectCount: number, simulations: Array<{ projectId: string, projectTitle: string, currentStatus: ProjectStatus, targetRoundId: string | null, targetRoundName: string, }>, } ``` ### Preserved Procedures (renamed only) - `delete` → `competition.delete` (archive) - `getSummary` → `competition.getSummary` (aggregated stats) - `getStageAnalytics` → `competition.getRoundAnalytics` (per-round stats) --- ## 2. Round Router (new, split from Stage) **File:** `src/server/routers/round.ts` Runtime operations for rounds (open/close windows, transition status, project states). ### New Procedures #### `get` ```typescript // Input schema z.object({ id: z.string() }) // Output { ...round, competition: { id, name, programId }, juryGroup: JuryGroup | null, submissionWindow: SubmissionWindow | null, visibleSubmissionWindows: Array, cohorts: Array, advancementRules: Array, stateDistribution: Record, _count: { projectRoundStates, assignments, evaluationForms }, } ``` #### `transition` **Purpose:** State machine for round status ```typescript // Input schema z.object({ id: z.string(), targetStatus: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']), }) // Validation: Enforces state machine rules // ROUND_DRAFT → ROUND_ACTIVE // ROUND_ACTIVE → ROUND_CLOSED // ROUND_CLOSED → ROUND_ARCHIVED | ROUND_ACTIVE (reopen) // Output: Round with new status ``` #### `openWindow` ```typescript // Input schema z.object({ id: z.string(), windowCloseAt: z.date().optional(), }) // Sets windowOpenAt = now, optionally sets windowCloseAt // Output: Round ``` #### `closeWindow` ```typescript // Input schema z.object({ id: z.string() }) // Sets windowCloseAt = now // Output: Round ``` #### `getProjectStates` **Purpose:** Paginated list of project states in a round ```typescript // Input schema z.object({ roundId: z.string(), state: z.enum([ 'PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN' ]).optional(), cursor: z.string().optional(), limit: z.number().int().min(1).max(100).default(50), }) // Output { items: Array<{ ...projectRoundState, project: { id, title, status, tags, teamName, competitionCategory }, }>, nextCursor: string | undefined, } ``` #### `getForJury` **Purpose:** Jury-facing round details ```typescript // Input schema z.object({ id: z.string() }) // Output { ...round, competition: { id, name, programId }, juryGroup: JuryGroup | null, evaluationForms: Array<{ id, criteriaJson, scalesJson }>, isWindowOpen: boolean, windowTimeRemaining: number | null, // milliseconds myAssignmentCount: number, myCompletedCount: number, } ``` #### `getApplicantTimeline` **Purpose:** Show a project's journey through rounds ```typescript // Input schema z.object({ projectId: z.string(), competitionId: z.string(), }) // Output Array<{ roundId: string, roundName: string, roundType: RoundType, state: ProjectRoundStateValue, enteredAt: DateTime, exitedAt: DateTime | null, isCurrent: boolean, }> ``` #### `getRequirements` **Purpose:** File requirements and upload status for a round ```typescript // Input schema z.object({ roundId: z.string(), projectId: z.string(), }) // Output { submissionWindow: { id: string, name: string, windowOpenAt: DateTime | null, windowCloseAt: DateTime | null, deadlinePolicy: DeadlinePolicy, } | null, fileRequirements: Array, uploadedFiles: Array, windowStatus: { isOpen: boolean, closesAt: DateTime | null, isLate: boolean, }, deadlineInfo: { windowOpenAt: DateTime | null, windowCloseAt: DateTime | null, graceHours: number, }, } ``` --- ## 3. Jury Group Router (new) **File:** `src/server/routers/jury-group.ts` ### New Procedures #### `create` ```typescript // Input schema z.object({ competitionId: z.string(), name: z.string().min(1).max(255), slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/), description: z.string().optional(), sortOrder: z.number().int().min(0).optional(), // Default assignment configuration defaultMaxAssignments: z.number().int().min(1).default(20), defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'), softCapBuffer: z.number().int().min(0).default(2), // Category quotas categoryQuotasEnabled: z.boolean().default(false), defaultCategoryQuotas: z.record(z.object({ min: z.number().int().min(0), max: z.number().int().min(1), })).optional(), // Onboarding settings allowJurorCapAdjustment: z.boolean().default(false), allowJurorRatioAdjustment: z.boolean().default(false), }) // Output: JuryGroup ``` #### `update` ```typescript // Input schema z.object({ id: z.string(), name: z.string().optional(), description: z.string().optional(), sortOrder: z.number().int().optional(), defaultMaxAssignments: z.number().int().optional(), defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).optional(), softCapBuffer: z.number().int().optional(), categoryQuotasEnabled: z.boolean().optional(), defaultCategoryQuotas: z.record(z.object({ min: z.number().int(), max: z.number().int(), })).optional(), allowJurorCapAdjustment: z.boolean().optional(), allowJurorRatioAdjustment: z.boolean().optional(), }) // Output: JuryGroup ``` #### `delete` ```typescript // Input schema z.object({ id: z.string() }) // Validation: Cannot delete if linked to any rounds or assignments // Output: { success: true } ``` #### `get` ```typescript // Input schema z.object({ id: z.string() }) // Output { ...juryGroup, competition: { id, name }, members: Array<{ ...juryGroupMember, user: { id, name, email, expertiseTags }, }>, rounds: Array<{ id, name, roundType }>, _count: { members, assignments }, } ``` #### `list` ```typescript // Input schema z.object({ competitionId: z.string() }) // Output Array<{ ...juryGroup, _count: { members, rounds, assignments }, }> ``` #### `addMember` ```typescript // Input schema z.object({ juryGroupId: z.string(), userId: z.string(), isLead: z.boolean().default(false), // Per-juror overrides maxAssignmentsOverride: z.number().int().optional(), capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional(), categoryQuotasOverride: z.record(z.object({ min: z.number().int(), max: z.number().int(), })).optional(), }) // Validation: User must have role JURY_MEMBER // Output: JuryGroupMember ``` #### `updateMember` ```typescript // Input schema z.object({ id: z.string(), // JuryGroupMember ID isLead: z.boolean().optional(), maxAssignmentsOverride: z.number().int().optional().nullable(), capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional().nullable(), categoryQuotasOverride: z.record(z.object({ min: z.number().int(), max: z.number().int(), })).optional().nullable(), // Juror preferences (self-service during onboarding) preferredStartupRatio: z.number().min(0).max(1).optional(), availabilityNotes: z.string().optional(), }) // Output: JuryGroupMember ``` #### `removeMember` ```typescript // Input schema z.object({ id: z.string() }) // Validation: Cannot remove if has active assignments // Output: { success: true } ``` #### `bulkAddMembers` ```typescript // Input schema z.object({ juryGroupId: z.string(), userIds: z.array(z.string()).min(1).max(100), }) // Creates JuryGroupMember for each user with default settings // Output: { created: number, skipped: number } ``` #### `getOnboarding` **Purpose:** Jury member views their onboarding settings for a group ```typescript // Input schema (protectedProcedure) z.object({ juryGroupId: z.string() }) // Output { juryGroup: JuryGroup, membership: JuryGroupMember, allowCapAdjustment: boolean, allowRatioAdjustment: boolean, hasCompleted: boolean, // Has filled preferences } ``` #### `submitOnboarding` **Purpose:** Jury member submits preferences ```typescript // Input schema (protectedProcedure) z.object({ juryGroupId: z.string(), // Conditionally allowed based on juryGroup.allowJurorCapAdjustment maxAssignmentsOverride: z.number().int().optional(), // Conditionally allowed based on juryGroup.allowJurorRatioAdjustment preferredStartupRatio: z.number().min(0).max(1).optional(), availabilityNotes: z.string().optional(), }) // Output: JuryGroupMember ``` --- ## 4. Submission Window Router (new) **File:** `src/server/routers/submission-window.ts` ### New Procedures #### `create` ```typescript // Input schema z.object({ competitionId: z.string(), name: z.string().min(1), slug: z.string().regex(/^[a-z0-9-]+$/), roundNumber: z.number().int().min(1), sortOrder: z.number().int().optional(), // Window timing windowOpenAt: z.date().optional(), windowCloseAt: z.date().optional(), // Deadline behavior deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'), graceHours: z.number().int().min(0).optional(), lockOnClose: z.boolean().default(true), }) // Output: SubmissionWindow ``` #### `update` ```typescript // Input schema z.object({ id: z.string(), name: z.string().optional(), windowOpenAt: z.date().optional().nullable(), windowCloseAt: z.date().optional().nullable(), deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).optional(), graceHours: z.number().int().optional(), lockOnClose: z.boolean().optional(), }) // Output: SubmissionWindow ``` #### `delete` ```typescript // Input schema z.object({ id: z.string() }) // Validation: Cannot delete if has uploaded files // Output: { success: true } ``` #### `get` ```typescript // Input schema z.object({ id: z.string() }) // Output { ...submissionWindow, competition: { id, name }, fileRequirements: Array, rounds: Array<{ id, name, roundType }>, _count: { fileRequirements, projectFiles }, } ``` #### `list` ```typescript // Input schema z.object({ competitionId: z.string() }) // Output Array<{ ...submissionWindow, _count: { fileRequirements, projectFiles, rounds }, }> ``` #### `addFileRequirement` ```typescript // Input schema z.object({ submissionWindowId: z.string(), name: z.string().min(1), description: z.string().optional(), acceptedMimeTypes: z.array(z.string()).min(1), maxSizeMB: z.number().int().min(1).optional(), isRequired: z.boolean().default(true), sortOrder: z.number().int().optional(), }) // Output: SubmissionFileRequirement ``` #### `updateFileRequirement` ```typescript // Input schema z.object({ id: z.string(), name: z.string().optional(), description: z.string().optional(), acceptedMimeTypes: z.array(z.string()).optional(), maxSizeMB: z.number().int().optional(), isRequired: z.boolean().optional(), sortOrder: z.number().int().optional(), }) // Output: SubmissionFileRequirement ``` #### `deleteFileRequirement` ```typescript // Input schema z.object({ id: z.string() }) // Validation: Cannot delete if has uploaded files against this requirement // Output: { success: true } ``` #### `setRoundVisibility` **Purpose:** Configure which submission windows a jury round can see ```typescript // Input schema z.object({ roundId: z.string(), submissionWindowIds: z.array(z.string()), // Windows jury can see displayLabels: z.record(z.string()).optional(), // submissionWindowId → label }) // Creates/updates RoundSubmissionVisibility records // Output: Array ``` --- ## 5. Winner Confirmation Router (new) **File:** `src/server/routers/winner-confirmation.ts` ### New Procedures #### `createProposal` **Purpose:** Admin or system generates a winner proposal ```typescript // Input schema z.object({ competitionId: z.string(), category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']), // Proposed rankings (ordered project IDs) rankedProjectIds: z.array(z.string()).min(1).max(10), // Evidence/basis sourceRoundId: z.string(), selectionBasis: z.object({ method: z.enum(['SCORE_BASED', 'VOTE_BASED', 'AI_RECOMMENDED', 'ADMIN_MANUAL']), scores: z.record(z.number()).optional(), aiRecommendation: z.string().optional(), reasoning: z.string().optional(), }), }) // Output: WinnerProposal (status: PENDING) ``` #### `getProposal` ```typescript // Input schema z.object({ id: z.string() }) // Output { ...winnerProposal, competition: { id, name }, sourceRound: { id, name, roundType }, proposedBy: { id, name, email }, frozenBy: { id, name } | null, overrideBy: { id, name } | null, approvals: Array<{ ...winnerApproval, user: { id, name, email }, }>, projects: Array<{ // Ordered by rankedProjectIds id, title, teamName, rank, }>, } ``` #### `listProposals` ```typescript // Input schema z.object({ competitionId: z.string(), status: z.enum([ 'PENDING', 'APPROVED', 'REJECTED', 'OVERRIDDEN', 'FROZEN' ]).optional(), }) // Output Array<{ ...winnerProposal, approvalProgress: { total: number, approved: number, rejected: number, pending: number, }, }> ``` #### `requestApproval` **Purpose:** Admin adds jury members to approval list ```typescript // Input schema z.object({ winnerProposalId: z.string(), userIds: z.array(z.string()).min(1), // Jury members to request approval from role: z.enum(['JURY_MEMBER', 'ADMIN']).default('JURY_MEMBER'), }) // Creates WinnerApproval records (approved: null) // Output: Array ``` #### `approve` **Purpose:** Jury member or admin approves the proposal ```typescript // Input schema (protectedProcedure) z.object({ winnerProposalId: z.string(), approved: z.boolean(), comments: z.string().optional(), }) // Updates WinnerApproval.approved = true/false, respondedAt = now // If all approvals received and all true → sets WinnerProposal.status = APPROVED // If any approval is false → sets WinnerProposal.status = REJECTED // Output: WinnerApproval ``` #### `getMyPendingApprovals` **Purpose:** Jury member sees proposals awaiting their approval ```typescript // Input schema (protectedProcedure) z.object({ competitionId: z.string().optional() }) // Output Array<{ ...winnerApproval, proposal: { ...winnerProposal, competition: { id, name }, projects: Array<{ id, title, rank }>, }, }> ``` #### `override` **Purpose:** Admin forces a result (bypasses jury approval) ```typescript // Input schema z.object({ winnerProposalId: z.string(), mode: z.enum(['FORCE_MAJORITY', 'ADMIN_DECISION']), reason: z.string().min(10), }) // Sets WinnerProposal.status = OVERRIDDEN // Sets overrideUsed = true, overrideMode, overrideReason, overrideById // Output: WinnerProposal ``` #### `freeze` **Purpose:** Lock the results (official, no further changes) ```typescript // Input schema z.object({ winnerProposalId: z.string() }) // Validation: Must be status APPROVED or OVERRIDDEN // Sets WinnerProposal.status = FROZEN, frozenAt = now, frozenById // Updates Competition with winnerProjectIds // Output: WinnerProposal ``` #### `unfreeze` **Purpose:** Admin unlocks results (rare, exceptional circumstances) ```typescript // Input schema z.object({ winnerProposalId: z.string(), reason: z.string().min(10), }) // Sets status = PENDING, frozenAt = null // Creates audit log with reason // Output: WinnerProposal ``` --- ## 6. Assignment Router (enhanced) **File:** `src/server/routers/assignment.ts` ### Modified Procedures #### `create` → Enhanced with jury group awareness **Changes:** Add `juryGroupId` parameter, enforce per-juror caps from JuryGroupMember ```typescript // Input schema z.object({ userId: z.string(), projectId: z.string(), roundId: z.string(), // RENAMED from stageId juryGroupId: z.string().optional(), // NEW: Link to jury group isRequired: z.boolean().default(true), forceOverride: z.boolean().default(false), }) // Validation: // - Checks JuryGroupMember.maxAssignmentsOverride if set // - Falls back to JuryGroup.defaultMaxAssignments // - Falls back to User.maxAssignments // - Enforces CapMode (HARD vs SOFT) // Output: Assignment (with juryGroupId) ``` #### `bulkCreate` → Enhanced with capacity checking **Changes:** Respect juryGroupId, apply category quotas ```typescript // Input schema z.object({ roundId: z.string(), // RENAMED from stageId juryGroupId: z.string().optional(), // NEW assignments: z.array(z.object({ userId: z.string(), projectId: z.string(), })), }) // Logic: // - For each juror, checks current count vs cap // - If CapMode = HARD → rejects over-cap assignments // - If CapMode = SOFT → allows up to softCapBuffer extra // - Applies category quotas if categoryQuotasEnabled // - Skips assignments that would violate quotas // Output { created: number, requested: number, skipped: number, skippedDueToCapacity: number, skippedDueToCategoryQuota: number, } ``` #### `getSuggestions` → Enhanced with category quotas **Changes:** Add quota scoring, penalty for over-quota categories ```typescript // Input schema z.object({ roundId: z.string(), // RENAMED from stageId juryGroupId: z.string().optional(), // NEW }) // Algorithm enhancements: // - Fetches JuryGroup.defaultCategoryQuotas // - Tracks per-juror category distribution // - Penalizes assignments to over-quota categories (-25 score) // - Bonuses assignments to under-quota categories (+10 score) // Output: Array of suggestions with quota awareness ``` ### New Procedures #### `getJuryGroupStats` **Purpose:** Per-group assignment coverage stats ```typescript // Input schema z.object({ juryGroupId: z.string(), roundId: z.string(), }) // Output { juryGroup: { id, name }, round: { id, name }, members: Array<{ user: { id, name, email }, currentAssignments: number, cap: number, capMode: CapMode, categoryDistribution: Record, // Category → count quotaStatus: Record, }>, totalAssignments: number, averageLoad: number, minLoad: number, maxLoad: number, } ``` --- ## 7. Evaluation Router (enhanced) **File:** `src/server/routers/evaluation.ts` ### Modified Procedures #### All procedures: Rename `stageId` → `roundId` in inputs/outputs #### `get`, `start`, `autosave`, `submit` — No functional changes, just renames ### New Procedures #### `getCrossRoundDocs` **Purpose:** Jury sees documents from multiple submission windows ```typescript // Input schema (protectedProcedure) z.object({ projectId: z.string(), roundId: z.string(), }) // Logic: // - Fetches RoundSubmissionVisibility records for roundId // - Returns files from all visible submission windows // Output { round: { id, name }, submissionWindows: Array<{ window: { id, name, roundNumber }, displayLabel: string, files: Array, requirements: Array, }>, } ``` --- ## 8. File Router (enhanced) **File:** `src/server/routers/file.ts` ### Modified Procedures #### `getDownloadUrl` → Enhanced for multi-round visibility **Changes:** Check RoundSubmissionVisibility for jury access ```typescript // Input schema (no changes) z.object({ bucket: z.string(), objectKey: z.string(), }) // Logic changes: // - If user is jury, checks Assignment.roundId // - Looks up RoundSubmissionVisibility for that round // - Grants access if file.submissionWindowId is in visible windows // Output: { url } ``` #### `getUploadUrl` → Enhanced for submission window association **Changes:** Link file to submissionWindowId ```typescript // Input schema z.object({ projectId: z.string(), fileName: z.string(), fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']), mimeType: z.string(), size: z.number().int().positive(), submissionWindowId: z.string().optional(), // NEW: Which window is this for? requirementId: z.string().optional(), // Links to SubmissionFileRequirement }) // Creates ProjectFile with submissionWindowId // Output: { uploadUrl, fileId } ``` ### New Procedures #### `promoteFromMentor` **Purpose:** Promote a MentorFile to official ProjectFile ```typescript // Input schema z.object({ mentorFileId: z.string(), submissionWindowId: z.string(), requirementId: z.string().optional(), }) // Logic: // - Copies object in MinIO to new key // - Creates ProjectFile linked to submissionWindow // - Sets MentorFile.isPromoted = true, promotedToFileId // Output: ProjectFile ``` --- ## 9. Applicant Router (enhanced) **File:** `src/server/routers/applicant.ts` ### Modified Procedures #### `getMySubmission` → Enhanced for multi-round **Changes:** Returns submission status for all rounds ```typescript // Input schema z.object({ competitionId: z.string().optional(), // NEW: Filter by competition programId: z.string().optional(), }) // Output { ...project, roundStates: Array<{ round: { id, name, roundType, sortOrder }, state: ProjectRoundStateValue, enteredAt: DateTime, exitedAt: DateTime | null, }>, currentRound: { id, name, roundType } | null, eligibleForNextSubmission: boolean, nextSubmissionWindow: SubmissionWindow | null, } ``` ### New Procedures #### `getCompetitionView` **Purpose:** Applicant's view of their journey through a competition ```typescript // Input schema (protectedProcedure) z.object({ competitionId: z.string(), projectId: z.string(), }) // Output { competition: { id, name, status }, project: { id, title, teamName, status }, rounds: Array<{ round: { id, name, roundType, sortOrder, windowOpenAt, windowCloseAt }, projectState: ProjectRoundState | null, isCurrent: boolean, isPassed: boolean, canSubmit: boolean, submissionWindow: SubmissionWindow | null, }>, timeline: Array<{ date: DateTime, event: string, description: string, }>, } ``` #### `submitToWindow` **Purpose:** Submit files for a specific submission window ```typescript // Input schema (protectedProcedure) z.object({ projectId: z.string(), submissionWindowId: z.string(), fileIds: z.array(z.string()), // ProjectFile IDs to mark as submitted }) // Validation: // - Window must be open // - All required FileRequirements must have files // - User must own project or be team member // Output: { success: true, submittedAt: DateTime } ``` --- ## 10. Live Control Router (enhanced) **File:** `src/server/routers/live-control.ts` (or `live.ts`) ### Modified Procedures #### All procedures: Rename `stageId` → `roundId` ### New Procedures #### `getStageManager` **Purpose:** Admin view of live ceremony control panel ```typescript // Input schema z.object({ roundId: z.string() }) // Output { round: { id, name, roundType }, liveCursor: LiveProgressCursor | null, liveVotingSession: LiveVotingSession | null, // Cohorts (project presentation order) cohorts: Array<{ ...cohort, projects: Array<{ project: { id, title, teamName }, presentationOrder: number, }>, }>, // Jury voting status juryVotes: Array<{ juror: { id, name }, votedProjects: Array, // Project IDs hasCompleted: boolean, }>, // Audience voting status audienceVotes: { total: number, byProject: Record, }, // Controls canStartVoting: boolean, canCloseVoting: boolean, canRevealResults: boolean, } ``` #### `setCursor` **Purpose:** Move live ceremony to a specific project/cohort ```typescript // Input schema z.object({ roundId: z.string(), cohortId: z.string().optional(), projectId: z.string().optional(), }) // Updates LiveProgressCursor.currentCohortId, currentProjectId // Output: LiveProgressCursor ``` --- ## 11. Mentor Workspace Router (new) **File:** `src/server/routers/mentor-workspace.ts` ### New Procedures #### `getWorkspace` **Purpose:** Mentor or applicant views the shared workspace ```typescript // Input schema (protectedProcedure) z.object({ mentorAssignmentId: z.string() }) // Validation: User must be mentor or project team member // Output { mentorAssignment: { ...assignment, mentor: { id, name, email }, project: { id, title, teamName }, workspaceEnabled: boolean, workspaceOpenAt: DateTime | null, workspaceCloseAt: DateTime | null, }, files: Array<{ ...mentorFile, uploadedBy: { id, name }, comments: Array, isPromoted: boolean, promotedToFile: { id, fileName } | null, }>, canUpload: boolean, canComment: boolean, canPromote: boolean, } ``` #### `uploadFile` ```typescript // Input schema (protectedProcedure) z.object({ mentorAssignmentId: z.string(), fileName: z.string(), mimeType: z.string(), size: z.number().int().positive(), description: z.string().optional(), }) // Creates MentorFile, returns pre-signed upload URL // Output: { uploadUrl, fileId } ``` #### `addComment` ```typescript // Input schema (protectedProcedure) z.object({ mentorFileId: z.string(), content: z.string().min(1).max(2000), parentCommentId: z.string().optional(), // For threading }) // Output: MentorFileComment ``` #### `promoteFile` **Purpose:** Mentor promotes a workspace file to official submission ```typescript // Input schema (mentorProcedure) z.object({ mentorFileId: z.string(), submissionWindowId: z.string(), requirementId: z.string().optional(), }) // Calls file.promoteFromMentor internally // Output: ProjectFile ``` --- ## 12. Special Award Router (renamed to Award, enhanced) **File:** `src/server/routers/award.ts` (renamed from `specialAward.ts`) ### Modified Procedures #### `create` → Enhanced for two-mode awards **Changes:** Add `eligibilityMode`, `juryGroupId`, `evaluationRoundId` ```typescript // Input schema z.object({ competitionId: z.string(), // CHANGED from programId name: z.string().min(1), description: z.string().optional(), criteriaText: z.string().optional(), // NEW: Award mode eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).default('STAY_IN_MAIN'), // Scoring/voting scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']), maxRankedPicks: z.number().int().optional(), // Decision decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).default('JURY_VOTE'), // NEW: Links to other entities evaluationRoundId: z.string().optional(), // Which round this award runs alongside juryGroupId: z.string().optional(), // Dedicated or shared jury group // AI eligibility useAiEligibility: z.boolean().default(false), autoTagRulesJson: z.record(z.unknown()).optional(), }) // Output: SpecialAward ``` #### `update` → Add new fields ```typescript // Input schema (all fields optional) z.object({ id: z.string(), name: z.string().optional(), description: z.string().optional(), criteriaText: z.string().optional(), eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).optional(), // NEW scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']).optional(), maxRankedPicks: z.number().int().optional(), decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).optional(), evaluationRoundId: z.string().optional(), // NEW juryGroupId: z.string().optional(), // NEW useAiEligibility: z.boolean().optional(), votingStartAt: z.date().optional(), votingEndAt: z.date().optional(), }) // Output: SpecialAward ``` ### New Procedures #### `getEligibilityByMode` **Purpose:** View eligible projects with mode-specific filtering ```typescript // Input schema z.object({ awardId: z.string() }) // Output { award: { ...specialAward, eligibilityMode }, eligibleProjects: Array<{ project: { id, title, teamName, competitionCategory }, eligibility: AwardEligibility, // For SEPARATE_POOL mode wasRemovedFromMain: boolean, // For STAY_IN_MAIN mode mainRoundStatus: ProjectRoundStateValue, }>, } ``` --- ## 13. Auth & User Router **File:** `src/server/routers/user.ts` ### New Procedures #### `listJuryMembers` **Purpose:** Admin lists all jury members for jury group assignment ```typescript // Input schema z.object({ programId: z.string().optional(), onlyActive: z.boolean().default(true), }) // Output Array<{ ...user, juryGroupMemberships: Array<{ juryGroup: { id, name, competition: { id, name } }, isLead: boolean, }>, _count: { assignments, juryGroupMemberships }, }> ``` --- ## Procedure Access Matrix | Router | Procedure | Public | Applicant | Jury | Mentor | Observer | Admin | Super Admin | |--------|-----------|--------|-----------|------|--------|----------|-------|-------------| | **competition** | create | | | | | | ✓ | ✓ | | | update | | | | | | ✓ | ✓ | | | delete | | | | | | ✓ | ✓ | | | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | list | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | publish | | | | | | ✓ | ✓ | | | createStructure | | | | | | ✓ | ✓ | | | updateStructure | | | | | | ✓ | ✓ | | | getDraft | | | | | | ✓ | ✓ | | | simulate | | | | | | ✓ | ✓ | | | getRoundAnalytics | | | | | ✓ | ✓ | ✓ | | **round** | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | | transition | | | | | | ✓ | ✓ | | | openWindow | | | | | | ✓ | ✓ | | | closeWindow | | | | | | ✓ | ✓ | | | getProjectStates | | | | | ✓ | ✓ | ✓ | | | getForJury | | | ✓ | | | | | | | getApplicantTimeline | | ✓ | ✓ | ✓ | | ✓ | ✓ | | | getRequirements | | ✓ | ✓ | ✓ | | ✓ | ✓ | | **jury-group** | create | | | | | | ✓ | ✓ | | | update | | | | | | ✓ | ✓ | | | delete | | | | | | ✓ | ✓ | | | get | | | ✓* | | ✓ | ✓ | ✓ | | | list | | | ✓* | | ✓ | ✓ | ✓ | | | addMember | | | | | | ✓ | ✓ | | | updateMember | | | | | | ✓ | ✓ | | | removeMember | | | | | | ✓ | ✓ | | | bulkAddMembers | | | | | | ✓ | ✓ | | | getOnboarding | | | ✓* | | | | | | | submitOnboarding | | | ✓* | | | | | | **submission-window** | create | | | | | | ✓ | ✓ | | | update | | | | | | ✓ | ✓ | | | delete | | | | | | ✓ | ✓ | | | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | | | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | | | addFileRequirement | | | | | | ✓ | ✓ | | | updateFileRequirement | | | | | | ✓ | ✓ | | | deleteFileRequirement | | | | | | ✓ | ✓ | | | setRoundVisibility | | | | | | ✓ | ✓ | | **winner-confirmation** | createProposal | | | | | | ✓ | ✓ | | | getProposal | | | ✓* | | ✓ | ✓ | ✓ | | | listProposals | | | | | ✓ | ✓ | ✓ | | | requestApproval | | | | | | ✓ | ✓ | | | approve | | | ✓* | | | ✓* | ✓ | | | getMyPendingApprovals | | | ✓* | | | ✓* | ✓ | | | override | | | | | | ✓ | ✓ | | | freeze | | | | | | ✓ | ✓ | | | unfreeze | | | | | | | ✓ | | **assignment** | create | | | | | | ✓ | ✓ | | | bulkCreate | | | | | | ✓ | ✓ | | | delete | | | | | | ✓ | ✓ | | | listByRound | | | | | ✓ | ✓ | ✓ | | | listByProject | | | | | ✓ | ✓ | ✓ | | | myAssignments | | | ✓ | | | | | | | get | | | ✓* | ✓* | | ✓ | ✓ | | | getStats | | | | | ✓ | ✓ | ✓ | | | getSuggestions | | | | | | ✓ | ✓ | | | getAISuggestions | | | | | | ✓ | ✓ | | | applySuggestions | | | | | | ✓ | ✓ | | | getJuryGroupStats | | | | | ✓ | ✓ | ✓ | | **evaluation** | get | | | ✓* | | | ✓ | ✓ | | | start | | | ✓* | | | | | | | autosave | | | ✓* | | | | | | | submit | | | ✓* | | | | | | | getProjectStats | | | | | ✓ | ✓ | ✓ | | | listByRound | | | | | ✓ | ✓ | ✓ | | | myPastEvaluations | | | ✓ | | | | | | | declareCOI | | | ✓* | | | | | | | getCOIStatus | | | ✓* | | | ✓ | ✓ | | | listCOIByRound | | | | | | ✓ | ✓ | | | reviewCOI | | | | | | ✓ | ✓ | | | generateSummary | | | | | | ✓ | ✓ | | | getSummary | | | | | ✓ | ✓ | ✓ | | | getCrossRoundDocs | | | ✓* | | | | | | **file** | getDownloadUrl | | ✓* | ✓* | ✓* | | ✓ | ✓ | | | getUploadUrl | | | | | | ✓ | ✓ | | | promoteFromMentor | | | | ✓* | | ✓ | ✓ | | **applicant** | getSubmissionBySlug | ✓ | ✓ | | | | | | | | getMySubmission | | ✓ | | | | | | | | saveSubmission | | ✓ | | | | | | | | getCompetitionView | | ✓* | | | | | | | | submitToWindow | | ✓* | | | | | | | **mentor-workspace** | getWorkspace | | ✓* | | ✓* | | ✓ | ✓ | | | uploadFile | | ✓* | | ✓* | | | | | | addComment | | ✓* | | ✓* | | | | | | promoteFile | | | | ✓ | | ✓ | ✓ | | **award** | create | | | | | | ✓ | ✓ | | | update | | | | | | ✓ | ✓ | | | delete | | | | | | ✓ | ✓ | | | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | | | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | | | updateStatus | | | | | | ✓ | ✓ | | | runEligibility | | | | | | ✓ | ✓ | | | getEligibilityByMode | | | | | ✓ | ✓ | ✓ | | **live-control** | getStageManager | | | | | | ✓ | ✓ | | | setCursor | | | | | | ✓ | ✓ | | | startVoting | | | | | | ✓ | ✓ | | | closeVoting | | | | | | ✓ | ✓ | | | castJuryVote | | | ✓* | | | | | | | castAudienceVote | | ✓* | ✓* | ✓* | ✓* | ✓* | ✓* | *Legend:* - `✓*` = Conditional access (must be assigned, own project, be in jury group, etc.) - `✓` = Unconditional access for that role --- ## Migration Checklist: tRPC Updates ### Phase 1: Rename Existing Routers - [ ] Rename `pipeline.ts` → `competition.ts` - [ ] Update all imports in UI components - [ ] Update all `trpc.pipeline.*` calls → `trpc.competition.*` ### Phase 2: Split Stage Router - [ ] Create `round.ts` for runtime operations - [ ] Move procedures: `transition`, `openWindow`, `closeWindow`, `getProjectStates`, `getForJury`, `getApplicantTimeline`, `getRequirements` - [ ] Update all `trpc.stage.*` calls to reference correct router ### Phase 3: Create New Routers - [ ] Create `jury-group.ts` - [ ] Create `winner-confirmation.ts` - [ ] Create `submission-window.ts` - [ ] Create `mentor-workspace.ts` ### Phase 4: Enhance Existing Routers - [ ] Update `assignment.ts` with jury group awareness - [ ] Update `evaluation.ts` with cross-round doc visibility - [ ] Update `file.ts` with submission window association - [ ] Update `applicant.ts` with multi-round support - [ ] Rename `specialAward.ts` → `award.ts` and enhance ### Phase 5: Update App Router - [ ] Update `src/server/routers/_app.ts` with new routers - [ ] Export all renamed routers - [ ] Run type generation: `npm run trpc:generate` --- **Total Procedures:** - **Current**: ~180 - **New**: ~60 - **Modified**: ~30 - **Removed**: ~20 - **Final**: ~250 This comprehensive reference ensures every API change is documented with complete type safety.